import 'dart:async';
import 'dart:io';

import 'package:http/http.dart' as http;
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';

import '../modelos/emisora.dart';

enum EstadoGrabacionRadioTipo {
  inactiva,
  preparando,
  grabando,
  deteniendo,
  error,
}

class EstadoGrabacionRadio {
  const EstadoGrabacionRadio({
    required this.tipo,
    this.emisora,
    this.archivo,
    this.bytes = 0,
    this.inicio,
    this.duracionObjetivo,
    this.error,
  });

  const EstadoGrabacionRadio.inactiva()
    : tipo = EstadoGrabacionRadioTipo.inactiva,
      emisora = null,
      archivo = null,
      bytes = 0,
      inicio = null,
      duracionObjetivo = null,
      error = null;

  final EstadoGrabacionRadioTipo tipo;
  final Emisora? emisora;
  final File? archivo;
  final int bytes;
  final DateTime? inicio;
  final Duration? duracionObjetivo;
  final String? error;

  bool get activa =>
      tipo == EstadoGrabacionRadioTipo.preparando ||
      tipo == EstadoGrabacionRadioTipo.grabando ||
      tipo == EstadoGrabacionRadioTipo.deteniendo;

  Duration get transcurrido {
    final inicioLocal = inicio;
    if (inicioLocal == null) return Duration.zero;
    return DateTime.now().difference(inicioLocal);
  }

  EstadoGrabacionRadio copyWith({
    EstadoGrabacionRadioTipo? tipo,
    Emisora? emisora,
    File? archivo,
    int? bytes,
    DateTime? inicio,
    Duration? duracionObjetivo,
    String? error,
  }) {
    return EstadoGrabacionRadio(
      tipo: tipo ?? this.tipo,
      emisora: emisora ?? this.emisora,
      archivo: archivo ?? this.archivo,
      bytes: bytes ?? this.bytes,
      inicio: inicio ?? this.inicio,
      duracionObjetivo: duracionObjetivo ?? this.duracionObjetivo,
      error: error,
    );
  }
}

class ServicioGrabacionRadio {
  ServicioGrabacionRadio({
    http.Client? cliente,
    Future<Directory> Function()? resolverDirectorioBase,
    DateTime Function()? reloj,
  }) : _clienteExterno = cliente,
       _resolverDirectorioBase = resolverDirectorioBase,
       _reloj = reloj ?? DateTime.now;

  static const _claveDirectorio = 'grabacion_radio_directorio';

  final http.Client? _clienteExterno;
  final Future<Directory> Function()? _resolverDirectorioBase;
  final DateTime Function() _reloj;
  final _estadoController = StreamController<EstadoGrabacionRadio>.broadcast();

  EstadoGrabacionRadio _estado = const EstadoGrabacionRadio.inactiva();
  StreamSubscription<List<int>>? _subscripcionStream;
  IOSink? _sink;
  http.Client? _clienteActivo;
  Timer? _timerAutoStop;
  String? _directorioConfigurado;

  EstadoGrabacionRadio get estado => _estado;
  Stream<EstadoGrabacionRadio> get estadoStream => _estadoController.stream;
  String? get directorioConfigurado => _directorioConfigurado;

  Future<void> inicializar() async {
    try {
      final prefs = await SharedPreferences.getInstance();
      _directorioConfigurado = prefs.getString(_claveDirectorio);
    } catch (_) {
      _directorioConfigurado = null;
    }
  }

  Future<String> directorioEfectivo() async {
    final configurado = _directorioConfigurado;
    if (configurado != null && configurado.trim().isNotEmpty) {
      return configurado;
    }
    final base =
        _resolverDirectorioBase != null
            ? await _resolverDirectorioBase()
            : await getApplicationDocumentsDirectory();
    return '${base.path}${Platform.pathSeparator}grabaciones';
  }

  Future<void> guardarDirectorio(String path) async {
    final normalizado = path.trim();
    if (normalizado.isEmpty) {
      throw ArgumentError('La ruta de grabación no puede estar vacía');
    }
    _directorioConfigurado = normalizado;
    try {
      final prefs = await SharedPreferences.getInstance();
      await prefs.setString(_claveDirectorio, normalizado);
    } catch (_) {}
    _emitir(_estado);
  }

  Future<void> limpiarDirectorioConfigurado() async {
    _directorioConfigurado = null;
    try {
      final prefs = await SharedPreferences.getInstance();
      await prefs.remove(_claveDirectorio);
    } catch (_) {}
    _emitir(_estado);
  }

  Future<void> iniciar(
    Emisora emisora, {
    Duration? duracion,
    String? directorio,
  }) async {
    if (_estado.activa) {
      throw StateError('Ya hay una grabación en curso');
    }

    final inicio = _reloj();
    _emitir(
      EstadoGrabacionRadio(
        tipo: EstadoGrabacionRadioTipo.preparando,
        emisora: emisora,
        inicio: inicio,
        duracionObjetivo: duracion,
      ),
    );

    try {
      final carpeta = Directory(directorio ?? await directorioEfectivo());
      await carpeta.create(recursive: true);

      final cliente = _clienteExterno ?? http.Client();
      _clienteActivo = cliente;
      final request = http.Request('GET', Uri.parse(emisora.url))
        ..headers['User-Agent'] = 'PluriWave/0.1.0 (radio recorder)';
      final response = await cliente.send(request);
      if (response.statusCode < 200 || response.statusCode >= 300) {
        throw HttpException('HTTP ${response.statusCode}', uri: request.url);
      }

      final archivo = File(
        '${carpeta.path}${Platform.pathSeparator}'
        '${_nombreArchivo(emisora, inicio, response.headers)}',
      );
      _sink = archivo.openWrite();
      _emitir(
        EstadoGrabacionRadio(
          tipo: EstadoGrabacionRadioTipo.grabando,
          emisora: emisora,
          archivo: archivo,
          inicio: inicio,
          duracionObjetivo: duracion,
        ),
      );

      if (duracion != null && duracion > Duration.zero) {
        _timerAutoStop = Timer(duracion, () => unawaited(detener()));
      }

      _subscripcionStream = response.stream.listen(
        (chunk) {
          _sink?.add(chunk);
          _emitir(_estado.copyWith(bytes: _estado.bytes + chunk.length));
        },
        onDone: () => unawaited(_finalizar()),
        onError: (Object error) => unawaited(_fallar(error)),
        cancelOnError: true,
      );
    } catch (error) {
      await _fallar(error);
      rethrow;
    }
  }

  Future<void> detener() async {
    if (!_estado.activa) return;
    _emitir(_estado.copyWith(tipo: EstadoGrabacionRadioTipo.deteniendo));
    _timerAutoStop?.cancel();
    _timerAutoStop = null;
    _clienteActivo?.close();
    await _subscripcionStream?.cancel();
    await _finalizar();
  }

  Future<void> _finalizar() async {
    _timerAutoStop?.cancel();
    _timerAutoStop = null;
    await _subscripcionStream?.cancel();
    _subscripcionStream = null;
    await _sink?.flush();
    await _sink?.close();
    _sink = null;
    if (_clienteExterno == null) {
      _clienteActivo?.close();
    }
    _clienteActivo = null;
    _emitir(const EstadoGrabacionRadio.inactiva());
  }

  Future<void> _fallar(Object error) async {
    _timerAutoStop?.cancel();
    _timerAutoStop = null;
    await _subscripcionStream?.cancel();
    _subscripcionStream = null;
    try {
      await _sink?.flush();
      await _sink?.close();
    } catch (_) {}
    _sink = null;
    if (_clienteExterno == null) {
      _clienteActivo?.close();
    }
    _clienteActivo = null;
    _emitir(
      EstadoGrabacionRadio(
        tipo: EstadoGrabacionRadioTipo.error,
        error: error.toString(),
      ),
    );
  }

  void _emitir(EstadoGrabacionRadio estado) {
    _estado = estado;
    if (!_estadoController.isClosed) {
      _estadoController.add(estado);
    }
  }

  String _nombreArchivo(
    Emisora emisora,
    DateTime inicio,
    Map<String, String> headers,
  ) {
    final fecha = inicio
        .toIso8601String()
        .replaceAll(':', '-')
        .replaceAll('.', '-');
    final nombre = _slug(emisora.nombre);
    final extension = _extension(emisora.codec, headers['content-type']);
    return '$fecha-$nombre.$extension';
  }

  String _extension(String? codec, String? contentType) {
    final c = codec?.toLowerCase().trim();
    if (c == 'mp3' || c == 'mpeg') return 'mp3';
    if (c == 'aac' || c == 'aac+' || c == 'heaac') return 'aac';
    if (c == 'ogg' || c == 'opus') return 'ogg';
    if (c == 'flac') return 'flac';

    final type = contentType?.toLowerCase() ?? '';
    if (type.contains('mpeg') || type.contains('mp3')) return 'mp3';
    if (type.contains('aac')) return 'aac';
    if (type.contains('ogg') || type.contains('opus')) return 'ogg';
    if (type.contains('flac')) return 'flac';
    return 'mp3';
  }

  String _slug(String value) {
    final slug = value
        .toLowerCase()
        .replaceAll(RegExp(r'[^a-z0-9áéíóúüñ]+', unicode: true), '-')
        .replaceAll(RegExp('-+'), '-')
        .replaceAll(RegExp(r'^-|-$'), '');
    return slug.isEmpty ? 'radio' : slug;
  }

  Future<void> dispose() async {
    _timerAutoStop?.cancel();
    _clienteActivo?.close();
    await _subscripcionStream?.cancel();
    await _sink?.close();
    await _estadoController.close();
  }
}
