// lib/widgets/cast/dlna.dart
//
// Implementazione DLNA minimale ma reale, pensata per funzionare
// con dlna_cast_controller.dart e upnp.dart, inclusi Sony Bravia.
//
// Espone:
// - DLNADevice
// - DLNAInfo
// - PlayType
// - DLNAHttp
//
// Richiede nel pubspec:
//   http:
//   xml:
//
// E usa:
//   lib/ref/upnp.dart
//

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

import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:xml/xml.dart';

import 'package:aves/ref/upnp.dart';

@immutable
class DLNAInfo {
  final String friendlyName;

  const DLNAInfo({
    required this.friendlyName,
  });
}

enum PlayType {
  Video,
  Image,
}

class DLNAHttp {
  const DLNAHttp._();

  static Future<http.Response> postSoap({
    required Uri url,
    required String serviceType,
    required String action,
    required String body,
    Duration timeout = const Duration(seconds: 5),
  }) async {
    final envelope = '''
<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
            s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
  <s:Body>
    $body
  </s:Body>
</s:Envelope>
''';

    final headers = <String, String>{
      'Content-Type': 'text/xml; charset="utf-8"',
      'SOAPACTION': '"$serviceType#$action"',
      'Connection': 'Close',
    };

    final resp = await http
        .post(url, headers: headers, body: utf8.encode(envelope))
        .timeout(timeout);

    if (resp.statusCode >= 400) {
      throw HttpException(
        'DLNA SOAP error ${resp.statusCode} for $action on $url',
        uri: url,
      );
    }

    return resp;
  }
}

class DLNADevice {
  final Uri avTransportUrl;
  final Uri connectionManagerUrl;
  final DLNAInfo info;

  DLNADevice({
    required this.avTransportUrl,
    required this.connectionManagerUrl,
    required this.info,
  });

  //──────────────────────────────────────────────
  // AVTransport: SetAVTransportURI
  //──────────────────────────────────────────────
  Future<void> setUrl(
    String url, {
    required String title,
    required PlayType type,
  }) async {
    // Molti renderer (inclusi Sony Bravia) accettano metadata vuoto.
    // Se servirà, si può estendere con DIDL-Lite completo.
    final escapedUrl = _xmlEscape(url);

    final body = '''
<u:SetAVTransportURI xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
  <InstanceID>0</InstanceID>
  <CurrentURI>$escapedUrl</CurrentURI>
  <CurrentURIMetaData></CurrentURIMetaData>
</u:SetAVTransportURI>
''';

    await DLNAHttp.postSoap(
      url: avTransportUrl,
      serviceType: 'urn:schemas-upnp-org:service:AVTransport:1',
      action: 'SetAVTransportURI',
      body: body,
    );
  }

  //──────────────────────────────────────────────
  // AVTransport: Play
  //──────────────────────────────────────────────
  Future<void> play() async {
    final body = '''
<u:Play xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
  <InstanceID>0</InstanceID>
  <Speed>1</Speed>
</u:Play>
''';

    await DLNAHttp.postSoap(
      url: avTransportUrl,
      serviceType: 'urn:schemas-upnp-org:service:AVTransport:1',
      action: 'Play',
      body: body,
    );
  }

  //──────────────────────────────────────────────
  // AVTransport: Stop
  //──────────────────────────────────────────────
  Future<void> stop() async {
    final body = '''
<u:Stop xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
  <InstanceID>0</InstanceID>
</u:Stop>
''';

    await DLNAHttp.postSoap(
      url: avTransportUrl,
      serviceType: 'urn:schemas-upnp-org:service:AVTransport:1',
      action: 'Stop',
      body: body,
    );
  }

  //──────────────────────────────────────────────
  // ConnectionManager: GetProtocolInfo
  //──────────────────────────────────────────────
  Future<Set<String>> getSinkSupportedMimeTypes() async {
    final body = Upnp.getProtocolInfoActionXml();

    final resp = await DLNAHttp.postSoap(
      url: connectionManagerUrl,
      serviceType: Upnp.upnpServiceTypeConnectionManager,
      action: 'GetProtocolInfo',
      body: _extractBody(body),
    );

    final text = resp.body;
    final doc = XmlDocument.parse(text);

    // Cerchiamo l'elemento che contiene il sink protocol info.
    // I nomi variano tra device, quindi cerchiamo per contenuto.
    final sinkNode = doc
        .findAllElements('Sink')
        .followedBy(doc.findAllElements('SinkProtocolInfo'))
        .cast<XmlElement?>()
        .firstWhere(
          (e) => e != null && e.innerText.trim().isNotEmpty,
          orElse: () => null,
        );

    if (sinkNode == null) {
      return <String>{};
    }

    final sinkText = sinkNode.innerText.trim();
    if (sinkText.isEmpty) {
      return <String>{};
    }

    final protocolInfo = UpnpProtocolInfo(sinkText);
    // Ritorniamo solo i MIME type (contentFormat).
    return protocolInfo.entries.map((e) => e.contentFormat).toSet();
  }

  //──────────────────────────────────────────────
  // Helper
  //──────────────────────────────────────────────
  static String _xmlEscape(String value) {
    return value
        .replaceAll('&', '&amp;')
        .replaceAll('<', '&lt;')
        .replaceAll('>', '&gt;')
        .replaceAll('"', '&quot;')
        .replaceAll("'", '&apos;');
  }

  // Upnp.getProtocolInfoActionXml() restituisce già un envelope completo.
  // Qui estraiamo solo il body interno per riusarlo nel nostro envelope.
  static String _extractBody(String envelope) {
    try {
      final doc = XmlDocument.parse(envelope);
      final body = doc.findAllElements('s:Body').firstOrNull ??
          doc.findAllElements('Body').first;
      return body.children.map((n) => n.toXmlString()).join();
    } catch (_) {
      // In caso di problemi, usiamo l'XML così com'è.
      return envelope;
    }
  }
}

extension _FirstOrNull<E> on Iterable<E> {
  E? get firstOrNull {
    final it = iterator;
    if (!it.moveNext()) return null;
    return it.current;
  }
}
