import 'dart:convert';

import '../../../contracts/danmaku.dart';
import '../../../contracts/error.dart';
import '../../../contracts/script_widget.dart';
import '../../../../shared/utils/app_logger.dart';
import '../../../../shared/utils/async_lock.dart';
import '../../errors.dart';
import '../../ports/danmaku_sources_store.dart';
import '../../ports/script_widget_remote_gateway.dart';
import '../../ports/script_widget_repository.dart';
import '../../ports/script_widget_runtime.dart';

const String scriptWidgetHostVersion = '0.0.2';

class ScriptWidgetContentSection {
  final String title;
  final List<ScriptVideoItem> items;

  const ScriptWidgetContentSection({required this.title, required this.items});
}

class ScriptWidgetService {
  static const String _tag = 'ScriptWidgetService';

  final ScriptWidgetRepository repository;
  final ScriptWidgetRemoteGateway remoteGateway;
  final ScriptWidgetRuntime runtime;
  final DanmakuSourcesStore? danmakuSourcesStore;
  final Future<void> Function()? onDanmakuSourcesChanged;
  final Map<String, AsyncLock> _widgetLocks = <String, AsyncLock>{};

  ScriptWidgetService({
    required this.repository,
    required this.remoteGateway,
    required this.runtime,
    this.danmakuSourcesStore,
    this.onDanmakuSourcesChanged,
  });

  Future<List<InstalledScriptWidget>> listWidgets() => repository.listWidgets();

  Future<List<ScriptWidgetSubscription>> listSubscriptions() =>
      repository.listSubscriptions();

  Future<InstalledScriptWidget> requireWidget(String widgetId) async {
    final widget = await repository.findWidget(widgetId);
    if (widget == null) {
      throw DomainError(ErrorCode.storeNotFound, '未找到模块：$widgetId');
    }
    return widget;
  }

  Future<InstalledScriptWidget> installFromUrl(String url) async {
    final scriptSource = await remoteGateway.fetchText(url);
    return installFromSource(scriptSource, sourceUrl: url);
  }

  Future<InstalledScriptWidget> installFromSource(
    String scriptSource, {
    String sourceUrl = '',
  }) async {
    final manifest = await runtime.inspectWidget(scriptSource);
    _assertCompatible(manifest);
    return _runWidgetOperation(
      manifest.id,
      () =>
          _installInspectedWidget(manifest, scriptSource, sourceUrl: sourceUrl),
    );
  }

  Future<ScriptWidgetSubscription> importSubscription(String url) async {
    final rawCatalog = await remoteGateway.fetchText(url);
    return importSubscriptionSource(rawCatalog, sourceId: url);
  }

  Future<ScriptWidgetSubscription> importSubscriptionSource(
    String rawCatalog, {
    required String sourceId,
  }) async {
    final decoded = jsonDecode(rawCatalog);
    if (decoded is! Map) {
      throw DomainError(ErrorCode.invalidParams, '订阅文件不是有效的 JSON 对象');
    }
    final decodedCatalog = ScriptWidgetCatalog.fromJson(
      Map<String, dynamic>.from(decoded),
    );
    if (decodedCatalog.title.trim().isEmpty) {
      throw DomainError(ErrorCode.invalidParams, '订阅缺少 title');
    }
    final catalog = decodedCatalog.copyWith(
      widgets: decodedCatalog.widgets
          .map((entry) => _resolveCatalogEntryUrl(entry, sourceId))
          .toList(growable: false),
    );
    final subscription = ScriptWidgetSubscription(
      url: sourceId,
      catalog: catalog,
      refreshedAt: DateTime.now().toUtc(),
    );
    await repository.upsertSubscription(subscription);
    return subscription;
  }

  Future<InstalledScriptWidget> refreshWidget(String widgetId) async {
    final widget = await requireWidget(widgetId);
    if (widget.sourceUrl.trim().isEmpty) {
      throw DomainError(ErrorCode.invalidParams, '本地导入模块没有可用的更新地址');
    }
    final scriptSource = await remoteGateway.fetchText(widget.sourceUrl);
    final manifest = await runtime.inspectWidget(scriptSource);
    if (manifest.id != widgetId) {
      throw DomainError(
        ErrorCode.invalidParams,
        '模块更新的 ID 不匹配：期望 $widgetId，实际为 ${manifest.id}',
      );
    }
    _assertCompatible(manifest);
    return _runWidgetOperation(
      widgetId,
      () => _installInspectedWidget(
        manifest,
        scriptSource,
        sourceUrl: widget.sourceUrl,
      ),
    );
  }

  Future<void> setWidgetEnabled(String widgetId, bool enabled) async {
    await _runWidgetOperation(widgetId, () async {
      final widget = await requireWidget(widgetId);
      final updated = widget.copyWith(
        enabled: enabled,
        updatedAt: DateTime.now().toUtc(),
      );
      await repository.upsertWidget(updated);
      await _synchronizeDanmakuSource(updated);
    });
  }

  Future<void> updateGlobalParams(
    String widgetId,
    Map<String, Object?> globalParams,
  ) async {
    await _runWidgetOperation(widgetId, () async {
      final widget = await requireWidget(widgetId);
      await repository.upsertWidget(
        widget.copyWith(
          globalParams: Map<String, Object?>.unmodifiable(globalParams),
          updatedAt: DateTime.now().toUtc(),
        ),
      );
    });
  }

  Future<void> deleteWidget(String widgetId) async {
    await _runWidgetOperation(widgetId, () async {
      await repository.deleteWidget(widgetId);
      await runtime.unloadWidget(widgetId);
      final sourceStore = danmakuSourcesStore;
      if (sourceStore != null) {
        final sources = await sourceStore.list();
        await sourceStore.replaceAll(
          sources.where((source) => source.url != 'jsw://$widgetId').toList(),
        );
        await onDanmakuSourcesChanged?.call();
      }
    });
  }

  Future<Object?> invoke(
    String widgetId,
    String functionName,
    Map<String, Object?> params,
  ) async {
    return _runWidgetOperation(widgetId, () async {
      final widget = await requireWidget(widgetId);
      if (!widget.enabled) {
        throw DomainError(
          ErrorCode.invalidParams,
          '模块已停用：${widget.manifest.title}',
        );
      }
      await _ensureLoaded(widget);
      final rawResult = await runtime.invokeModuleFunction(
        widgetId,
        functionName,
        <String, Object?>{...widget.globalParams, ...params},
      );
      AppLogger.debug(
        _tag,
        '[$widgetId] $functionName -> ${_summarizeInvocationResult(rawResult)}',
      );
      return rawResult;
    });
  }

  Future<List<ScriptWidgetContentSection>> loadModuleSections(
    String widgetId,
    ScriptWidgetModule module,
    Map<String, Object?> params,
  ) async {
    final resolvedParams = <String, Object?>{
      ...buildScriptWidgetParameterDefaults(module.params),
      ...params,
    };
    final rawResult = await invoke(
      widgetId,
      module.functionName,
      resolvedParams,
    );
    return decodeVideoSections(
      rawResult,
      fallbackTitle: module.title,
      preserveSections: module.sectionMode,
    );
  }

  Future<List<ScriptVideoItem>> search(
    String widgetId,
    Map<String, Object?> params,
  ) async {
    final widget = await requireWidget(widgetId);
    final searchModule = widget.manifest.search;
    if (searchModule == null) return const <ScriptVideoItem>[];
    final rawResult = await invoke(widgetId, searchModule.functionName, params);
    return decodeVideoItems(rawResult);
  }

  Future<ScriptVideoItem?> loadDetail(String widgetId, String link) async {
    final rawResult = await invoke(widgetId, 'loadDetail', <String, Object?>{
      '__forwardRawArgument': link,
      'link': link,
    });
    final items = decodeVideoItems(rawResult);
    return items.isEmpty ? null : items.first;
  }

  Future<List<ScriptVideoResource>> loadResources(
    String widgetId,
    Map<String, Object?> params,
  ) async {
    final widget = await requireWidget(widgetId);
    final resourceModule = widget.manifest.modules
        .where(
          (module) => module.id == 'loadResource' || module.type == 'stream',
        )
        .firstOrNull;
    if (resourceModule == null) return const <ScriptVideoResource>[];
    final rawResult = await invoke(
      widgetId,
      resourceModule.functionName,
      params,
    );
    return _decodeMapList(rawResult)
        .map(ScriptVideoResource.fromJson)
        .where((resource) => resource.url.trim().isNotEmpty)
        .toList(growable: false);
  }

  Future<void> _ensureLoaded(InstalledScriptWidget widget) async {
    final widgetId = widget.manifest.id;
    if (runtime.isWidgetLoaded(widgetId)) return;
    final loadedManifest = await runtime.loadWidget(widget.scriptSource);
    if (loadedManifest.id != widgetId) {
      await runtime.unloadWidget(loadedManifest.id);
      throw DomainError(
        ErrorCode.invalidParams,
        '已安装模块的脚本 ID 不匹配：期望 $widgetId，实际为 ${loadedManifest.id}',
      );
    }
  }

  Future<InstalledScriptWidget> _installInspectedWidget(
    ScriptWidgetManifest manifest,
    String scriptSource, {
    required String sourceUrl,
  }) async {
    final existingWidget = await repository.findWidget(manifest.id);
    final existingDanmakuSources = danmakuSourcesStore == null
        ? null
        : await danmakuSourcesStore!.list();
    final now = DateTime.now().toUtc();
    final globalParams = <String, Object?>{
      for (final parameter in manifest.globalParams)
        parameter.name:
            existingWidget?.globalParams[parameter.name] ?? parameter.value,
    };
    final installedWidget = InstalledScriptWidget(
      manifest: manifest,
      scriptSource: scriptSource,
      sourceUrl: sourceUrl,
      enabled: existingWidget?.enabled ?? true,
      globalParams: globalParams,
      installedAt: existingWidget?.installedAt ?? now,
      updatedAt: now,
    );

    try {
      final loadedManifest = await runtime.loadWidget(scriptSource);
      if (loadedManifest.id != manifest.id) {
        await runtime.unloadWidget(loadedManifest.id);
        throw DomainError(
          ErrorCode.invalidParams,
          '模块脚本在检查与加载期间返回了不同的 ID',
          details: <String, Object?>{
            'inspectedId': manifest.id,
            'loadedId': loadedManifest.id,
          },
        );
      }
      await repository.upsertWidget(installedWidget);
      await _synchronizeDanmakuSource(installedWidget);
      return installedWidget;
    } catch (error, stackTrace) {
      await _restoreInstallation(
        manifest.id,
        existingWidget,
        existingDanmakuSources,
      );
      Error.throwWithStackTrace(error, stackTrace);
    }
  }

  Future<void> _restoreInstallation(
    String widgetId,
    InstalledScriptWidget? existingWidget,
    List<DanmakuSource>? existingDanmakuSources,
  ) async {
    await _ignoreRollbackFailure(() async {
      if (existingWidget == null) {
        await repository.deleteWidget(widgetId);
      } else {
        await repository.upsertWidget(existingWidget);
      }
    });
    await _ignoreRollbackFailure(() async {
      if (existingWidget == null) {
        await runtime.unloadWidget(widgetId);
      } else {
        var restoredExistingRuntime = false;
        try {
          final restoredManifest = await runtime.loadWidget(
            existingWidget.scriptSource,
          );
          restoredExistingRuntime = restoredManifest.id == widgetId;
          if (!restoredExistingRuntime) {
            await runtime.unloadWidget(restoredManifest.id);
          }
        } finally {
          if (!restoredExistingRuntime) {
            await runtime.unloadWidget(widgetId);
          }
        }
      }
    });
    final sourceStore = danmakuSourcesStore;
    if (sourceStore != null && existingDanmakuSources != null) {
      await _ignoreRollbackFailure(() async {
        await sourceStore.replaceAll(existingDanmakuSources);
        await onDanmakuSourcesChanged?.call();
      });
    }
  }

  Future<T> _runWidgetOperation<T>(
    String widgetId,
    Future<T> Function() operation,
  ) {


    return _widgetLocks.putIfAbsent(widgetId, AsyncLock.new).run(operation);
  }

  Future<void> _ignoreRollbackFailure(Future<void> Function() rollback) async {
    try {
      await rollback();
    } catch (_) {}
  }

  ScriptWidgetCatalogEntry _resolveCatalogEntryUrl(
    ScriptWidgetCatalogEntry entry,
    String sourceId,
  ) {
    final entryUri = Uri.tryParse(entry.url);
    if (entryUri == null || entryUri.hasScheme) return entry;
    final sourceUri = Uri.tryParse(sourceId);
    if (sourceUri == null || !sourceUri.hasScheme) return entry;
    return entry.copyWith(url: sourceUri.resolveUri(entryUri).toString());
  }

  void _assertCompatible(ScriptWidgetManifest manifest) {
    if (_compareVersions(manifest.requiredVersion, scriptWidgetHostVersion) >
        0) {
      throw DomainError(
        ErrorCode.invalidParams,
        '模块需要 ForwardWidgets ${manifest.requiredVersion}，当前兼容版本为 $scriptWidgetHostVersion',
      );
    }
    if (manifest.modules.any((module) => module.requiresWebView)) {
      throw DomainError(ErrorCode.invalidParams, '当前版本暂不支持 requiresWebView 模块');
    }
  }

  Future<void> _synchronizeDanmakuSource(InstalledScriptWidget widget) async {
    final sourceStore = danmakuSourcesStore;
    if (sourceStore == null) return;
    final hasDanmaku = widget.manifest.modules.any(
      (module) => module.type == 'danmu',
    );
    final sourceUrl = 'jsw://${widget.manifest.id}';
    final sources = await sourceStore.list();
    sources.removeWhere((source) => source.url == sourceUrl);
    if (hasDanmaku) {
      sources.add(
        DanmakuSource(
          id: 'script-widget-${widget.manifest.id}',
          name: widget.manifest.title,
          url: sourceUrl,
          enabled: widget.enabled,
        ),
      );
    }
    await sourceStore.replaceAll(sources);
    await onDanmakuSourcesChanged?.call();
  }
}

List<ScriptVideoItem> decodeVideoItems(Object? rawResult) {
  final maps = _decodeMapList(rawResult);
  if (maps.isEmpty) {
    if (_isExplicitlyEmptyModuleResult(rawResult)) {
      return const <ScriptVideoItem>[];
    }
    throw DomainError(
      ErrorCode.invalidParams,
      '模块返回了无法识别的内容格式',
      details: <String, Object?>{
        'resultType': rawResult.runtimeType.toString(),
        'result': _summarizeModuleResult(rawResult),
      },
    );
  }

  final items = <ScriptVideoItem>[];
  final decodingErrors = <String>[];
  for (final map in maps) {
    if (map['items'] is List && !map.containsKey('id')) {
      try {
        items.addAll(decodeVideoItems(map['items']));
      } catch (error) {
        decodingErrors.add(error.toString());
      }
      continue;
    }
    try {
      items.add(ScriptVideoItem.fromJson(_normalizeVideoItemMap(map)));
    } catch (error) {
      decodingErrors.add(error.toString());
    }
  }

  if (items.isEmpty && decodingErrors.isNotEmpty) {
    throw DomainError(
      ErrorCode.invalidParams,
      '模块返回了内容，但字段格式与 ForwardWidgets 契约不兼容',
      details: <String, Object?>{
        'itemCount': maps.length,
        'firstItem': _summarizeModuleResult(maps.first),
        'firstError': decodingErrors.first,
      },
    );
  }
  return items;
}

Map<String, dynamic> _normalizeVideoItemMap(Map<String, dynamic> source) {
  final normalized = Map<String, dynamic>.from(source);
  final fallbackId =
      normalized['link'] ??
      normalized['videoUrl'] ??
      normalized['url'] ??
      normalized['title'];
  normalized['id'] = normalized['id'] ?? fallbackId ?? '';
  const stringFieldNames = <String>{
    'id',
    'type',
    'title',
    'coverUrl',
    'posterPath',
    'posterUrl',
    'poster_url',
    'detailPoster',
    'backdropPath',
    'releaseDate',
    'mediaType',
    'genreTitle',
    'durationText',
    'previewUrl',
    'videoUrl',
    'link',
    'description',
    'playerType',
  };
  for (final fieldName in stringFieldNames) {
    final value = normalized[fieldName];
    if (value != null) normalized[fieldName] = value.toString();
  }
  normalized['title'] ??= '';
  normalized['type'] ??= 'link';

  final subtitle = normalized['subTitle'] ?? normalized['subtitle'];
  if (subtitle != null &&
      (normalized['description'] == null ||
          normalized['description'].toString().isEmpty)) {
    normalized['description'] = subtitle.toString();
  }
  return normalized;
}

bool _isExplicitlyEmptyModuleResult(Object? rawResult) {
  if (rawResult is List) return rawResult.isEmpty;
  if (rawResult is Map) return rawResult.isEmpty;
  return false;
}

String _summarizeModuleResult(Object? rawResult) {
  final summary = rawResult?.toString() ?? 'null';
  const maximumSummaryLength = 500;
  if (summary.length <= maximumSummaryLength) return summary;
  return '${summary.substring(0, maximumSummaryLength)}…';
}

List<ScriptWidgetContentSection> decodeVideoSections(
  Object? rawResult, {
  required String fallbackTitle,
  required bool preserveSections,
}) {
  if (preserveSections && rawResult is List) {
    final sections = <ScriptWidgetContentSection>[];
    for (final rawSection in rawResult.whereType<Map>()) {
      final section = Map<String, dynamic>.from(rawSection);
      final rawItems =
          section['items'] ?? section['data'] ?? section['results'];
      if (rawItems == null) continue;
      final items = decodeVideoItems(rawItems);
      if (items.isEmpty) continue;
      sections.add(
        ScriptWidgetContentSection(
          title:
              section['title']?.toString() ??
              section['name']?.toString() ??
              fallbackTitle,
          items: items,
        ),
      );
    }
    if (sections.isNotEmpty) return sections;
  }

  final items = decodeVideoItems(rawResult);
  if (items.isEmpty) return const <ScriptWidgetContentSection>[];
  return <ScriptWidgetContentSection>[
    ScriptWidgetContentSection(title: fallbackTitle, items: items),
  ];
}

List<Map<String, dynamic>> _decodeMapList(Object? rawResult) {
  if (rawResult is String) {
    final trimmedResult = rawResult.trim();
    if (trimmedResult.isEmpty) return const <Map<String, dynamic>>[];
    try {
      return _decodeMapList(jsonDecode(trimmedResult));
    } catch (_) {
      return const <Map<String, dynamic>>[];
    }
  }
  if (rawResult is Map) {
    final map = Map<String, dynamic>.from(rawResult);
    final nested = map['items'] ?? map['results'] ?? map['data'];
    if (!map.containsKey('id') && nested != null) {
      return _decodeMapList(nested);
    }
    return <Map<String, dynamic>>[map];
  }
  if (rawResult is! List) return const <Map<String, dynamic>>[];
  return rawResult
      .whereType<Map>()
      .map(Map<String, dynamic>.from)
      .toList(growable: false);
}

int _compareVersions(String left, String right) {
  final leftParts = _versionParts(left);
  final rightParts = _versionParts(right);
  final length = leftParts.length > rightParts.length
      ? leftParts.length
      : rightParts.length;
  for (var index = 0; index < length; index++) {
    final leftValue = index < leftParts.length ? leftParts[index] : 0;
    final rightValue = index < rightParts.length ? rightParts[index] : 0;
    if (leftValue != rightValue) return leftValue.compareTo(rightValue);
  }
  return 0;
}

List<int> _versionParts(String version) {
  return version
      .split('.')
      .map((part) => int.tryParse(RegExp(r'\d+').stringMatch(part) ?? '') ?? 0)
      .toList(growable: false);
}

String _summarizeInvocationResult(Object? rawResult) {
  if (rawResult == null) return 'null';
  if (rawResult is List) {
    return 'List(length=${rawResult.length})';
  }
  if (rawResult is Map) {
    return 'Map(keys=${rawResult.keys.take(6).toList()}, size=${rawResult.length})';
  }
  final summary = rawResult.toString();
  const maximumSummaryLength = 200;
  if (summary.length <= maximumSummaryLength) {
    return '${rawResult.runtimeType} $summary';
  }
  return '${rawResult.runtimeType} ${summary.substring(0, maximumSummaryLength)}…';
}
