import 'dart:convert';

import 'package:flutter/foundation.dart' show visibleForTesting;
import 'package:shared_preferences/shared_preferences.dart';

import '../../../shared/utils/async_lock.dart';
import '../../../shared/utils/app_logger.dart';
import 'safe_script_http_client.dart';

typedef WidgetHostBootstrapData = ({
  Map<String, Object?> storage,
  Map<String, Object?> sharedCache,
});

const String _browserUserAgent =
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
    'AppleWebKit/537.36 (KHTML, like Gecko) '
    'Chrome/131.0.0.0 Safari/537.36';

class WidgetHostBridge {
  static const String _tag = 'ScriptWidgetHost';
  static const String _storagePrefix = 'smplayer.script_widget.storage.';
  static const String _sharedCachePrefix =
      'smplayer.script_widget.shared_cache.';

  final SafeScriptHttpClient _httpClient;
  final Map<String, AsyncLock> _persistenceLocks = <String, AsyncLock>{};

  WidgetHostBridge({SafeScriptHttpClient? httpClient})
    : _httpClient = httpClient ?? SafeScriptHttpClient();

  Future<WidgetHostBootstrapData> loadBootstrapData(String widgetId) async {
    final preferences = await SharedPreferences.getInstance();
    return (
      storage: _decodeObjectMap(
        preferences.getString('$_storagePrefix$widgetId'),
      ),
      sharedCache: _decodeObjectMap(
        preferences.getString('$_sharedCachePrefix$widgetId'),
      ),
    );
  }


  Future<Object?> handleMessage(
    String widgetId,
    String channel,
    Map<String, dynamic> payload,
  ) async {
    switch (channel) {
      case 'WidgetHttp':
        return _handleHttpRequest(payload);
      case 'WidgetStoragePersist':
        return _persistJson(
          '$_storagePrefix$widgetId',
          _asMap(payload['storage']),
        );
      case 'WidgetSharedCachePersist':
        return _persistJson(
          '$_sharedCachePrefix$widgetId',
          _asMap(payload['sharedCache']),
        );
      case 'WidgetConsole':
        final level = payload['level']?.toString() ?? 'log';
        final rawValues = payload['values'];
        final message = rawValues is List
            ? rawValues.map(formatConsoleValue).join(' ')
            : formatConsoleValue(rawValues);
        switch (level) {
          case 'error':
            AppLogger.error(_tag, '[$widgetId] $message', message);
          case 'warn':
            AppLogger.warn(_tag, '[$widgetId] $message');
          case 'info':
            AppLogger.info(_tag, '[$widgetId] $message');
          default:
            AppLogger.debug(_tag, '[$widgetId] $message');
        }
        return true;
      default:
        throw ArgumentError('未知的宿主通道：$channel');
    }
  }

  Future<Map<String, Object?>> _handleHttpRequest(
    Map<String, dynamic> request,
  ) async {
    final method = request['method']?.toString().toUpperCase() ?? 'GET';
    final url = request['url']?.toString() ?? '';
    final uri = Uri.tryParse(url);
    if (uri == null) {
      throw ArgumentError('Widget.http only permits HTTP(S) URLs');
    }
    final optionsMap = _asMap(request['options']);
    final scriptHeaders = <String, String>{
      for (final entry in _asMap(optionsMap['headers']).entries)
        entry.key: entry.value.toString(),
    };
    final hasUserAgent = scriptHeaders.keys.any(
      (headerName) => headerName.toLowerCase() == 'user-agent',
    );
    final headers = <String, String>{
      if (!hasUserAgent) 'User-Agent': _browserUserAgent,
      ...scriptHeaders,
    };
    final queryParameters = <String, Object?>{
      for (final entry in _asMap(optionsMap['params']).entries)
        entry.key: entry.value,
    };
    final response = await _httpClient.request(
      uri: uri,
      method: method,
      headers: headers,
      queryParameters: queryParameters,
      body: request['body'],
      followRedirects: optionsMap['allow_redirects'] != false,
    );
    return <String, Object?>{
      'data': response.data,
      'status': response.statusCode,


      'statusCode': response.statusCode,
      'statusText': response.reasonPhrase,
      'headers': response.headers,
    };
  }

  Future<bool> _persistJson(
    String preferenceKey,
    Map<String, dynamic> value,
  ) => _persistenceLocks.putIfAbsent(preferenceKey, AsyncLock.new).run(() async {
    final preferences = await SharedPreferences.getInstance();
    return preferences.setString(preferenceKey, jsonEncode(value));
  });

  void clearWidget(String widgetId) {
    _persistenceLocks.remove('$_storagePrefix$widgetId');
    _persistenceLocks.remove('$_sharedCachePrefix$widgetId');
  }

  void clear() => _persistenceLocks.clear();
}

Map<String, dynamic> _asMap(Object? value) {
  if (value is Map<String, dynamic>) return value;
  if (value is Map) return Map<String, dynamic>.from(value);
  if (value is String) {
    try {
      final decodedValue = jsonDecode(value);
      if (decodedValue is Map) {
        return Map<String, dynamic>.from(decodedValue);
      }
    } catch (_) {}
  }
  return <String, dynamic>{};
}

Map<String, Object?> _decodeObjectMap(String? rawJson) {
  if (rawJson == null || rawJson.trim().isEmpty) return <String, Object?>{};
  try {
    return _asMap(jsonDecode(rawJson));
  } catch (_) {
    return <String, Object?>{};
  }
}

@visibleForTesting
String formatConsoleValue(Object? value) {
  if (value == null) return 'null';
  if (value is String) return value;
  if (value is num || value is bool) return value.toString();
  if (value is Map) {
    if (value['__consoleErrorMarker'] == true) {
      final errorName = value['name']?.toString() ?? 'Error';
      final errorMessage = value['message']?.toString() ?? '';
      final errorStack = value['stack']?.toString() ?? '';
      final formatted = errorMessage.isEmpty
          ? errorName
          : '$errorName: $errorMessage';
      return errorStack.isEmpty ? formatted : '$formatted\n$errorStack';
    }
    try {
      return jsonEncode(value);
    } catch (_) {
      return value.toString();
    }
  }
  if (value is Iterable) {
    try {
      return jsonEncode(value.toList(growable: false));
    } catch (_) {
      return value.toString();
    }
  }
  return value.toString();
}
