part of '../../core_app.dart';

const double _pickedImageMaxWidth = 1600;
const double _pickedImageMaxHeight = 1600;
const int _pickedImageQuality = 85;
const Set<String> _imageFileExtensions = <String>{
  'png',
  'jpg',
  'jpeg',
  'webp',
  'gif',
  'bmp',
  'heic',
  'heif',
};
const Set<String> _videoFileExtensions = <String>{
  'mp4',
  'mov',
  'avi',
  'wmv',
  'mkv',
  'm4v',
  'webm',
  '3gp',
};

Future<XFile?> _pickCameraImage(
  ImagePicker imagePicker, {
  bool showPermissionAlert = false,
  bool preserveOriginal = false,
  WebViewController? controller,
}) async {
  final cameraStatus = await Permission.camera.request();
  if (!cameraStatus.isGranted) {
    if (showPermissionAlert && controller != null) {
      await _showWebAlert(controller, '请先在系统设置中允许相机权限');
    }
    return null;
  }

  try {
    if (preserveOriginal) {
      return await imagePicker.pickImage(source: ImageSource.camera);
    }
    return await imagePicker.pickImage(
      source: ImageSource.camera,
      imageQuality: _pickedImageQuality,
      maxWidth: _pickedImageMaxWidth,
      maxHeight: _pickedImageMaxHeight,
    );
  } on Object catch (error, stackTrace) {
    debugPrint('调用相机拍照失败：$error\n$stackTrace');
    if (showPermissionAlert && controller != null) {
      await _showWebAlert(controller, '无法打开系统相机，请稍后重试');
    }
    return null;
  }
}

Future<XFile?> _pickCameraVideo(
  ImagePicker imagePicker, {
  bool showPermissionAlert = false,
  WebViewController? controller,
}) async {
  final statuses = await <Permission>[
    Permission.camera,
    Permission.microphone,
  ].request();

  final granted = statuses.values.every((status) => status.isGranted);

  if (!granted) {
    if (showPermissionAlert && controller != null) {
      await _showWebAlert(controller, '请先在系统设置中允许相机和麦克风权限');
    }
    return null;
  }

  try {
    return await imagePicker.pickVideo(
      source: ImageSource.camera,
    );
  } on Object catch (error, stackTrace) {
    debugPrint('调用相机录像失败：$error\n$stackTrace');
    if (showPermissionAlert && controller != null) {
      await _showWebAlert(controller, '无法打开系统相机录像，请稍后重试');
    }
    return null;
  }
}

Future<List<XFile>> _pickGalleryImages(
  ImagePicker imagePicker, {
  required bool multiple,
  bool preserveOriginal = false,
}) async {
  if (multiple) {
    if (preserveOriginal) {
      return imagePicker.pickMultiImage();
    }
    return imagePicker.pickMultiImage(
      imageQuality: _pickedImageQuality,
      maxWidth: _pickedImageMaxWidth,
      maxHeight: _pickedImageMaxHeight,
    );
  }

  if (preserveOriginal) {
    final file = await imagePicker.pickImage(source: ImageSource.gallery);
    return file == null ? const <XFile>[] : <XFile>[file];
  }

  final file = await imagePicker.pickImage(
    source: ImageSource.gallery,
    imageQuality: _pickedImageQuality,
    maxWidth: _pickedImageMaxWidth,
    maxHeight: _pickedImageMaxHeight,
  );
  return file == null ? const <XFile>[] : <XFile>[file];
}

Future<List<XFile>> _pickGalleryVideos(
  ImagePicker imagePicker, {
  required bool multiple,
}) async {
  if (multiple) {
    return imagePicker.pickMultiVideo();
  }

  final file = await imagePicker.pickVideo(
    source: ImageSource.gallery,
  );
  return file == null ? const <XFile>[] : <XFile>[file];
}

Future<List<XFile>> _pickGalleryMedia(
  ImagePicker imagePicker, {
  required bool multiple,
  bool preserveOriginal = false,
}) async {
  if (multiple) {
    if (preserveOriginal) {
      return imagePicker.pickMultipleMedia();
    }
    return imagePicker.pickMultipleMedia(
      imageQuality: _pickedImageQuality,
      maxWidth: _pickedImageMaxWidth,
      maxHeight: _pickedImageMaxHeight,
    );
  }

  if (preserveOriginal) {
    final file = await imagePicker.pickMedia();
    return file == null ? const <XFile>[] : <XFile>[file];
  }

  final file = await imagePicker.pickMedia(
    imageQuality: _pickedImageQuality,
    maxWidth: _pickedImageMaxWidth,
    maxHeight: _pickedImageMaxHeight,
  );
  return file == null ? const <XFile>[] : <XFile>[file];
}

Future<List<Map<String, dynamic>>> _serializeXFiles(
  List<XFile> files, {
  required String responseType,
}) async {
  final includeBase64 = responseType == 'base64' || responseType == 'dataUrl';
  final includeDataUrl = responseType == 'dataUrl';

  final serialized = <Map<String, dynamic>>[];
  for (final file in files) {
    String? base64Value;
    final mimeType = _guessMimeType(file.name);

    if (includeBase64 || includeDataUrl) {
      base64Value = base64Encode(await file.readAsBytes());
    }

    serialized.add(<String, dynamic>{
      'name': file.name,
      'uri': Uri.file(file.path).toString(),
      'mimeType': mimeType,
      'size': await file.length(),
      if (responseType == 'base64') 'base64': base64Value,
      if (includeDataUrl && base64Value != null)
        'dataUrl': 'data:$mimeType;base64,$base64Value',
    });
  }
  return serialized;
}

Future<List<Map<String, dynamic>>> _serializePlatformFiles(
  List<PlatformFile> files, {
  required String responseType,
}) async {
  final includeBase64 = responseType == 'base64' || responseType == 'dataUrl';
  final includeDataUrl = responseType == 'dataUrl';

  final serialized = <Map<String, dynamic>>[];
  for (final file in files) {
    final mimeType = _guessMimeType(file.name);
    String? base64Value;

    if (includeBase64 || includeDataUrl) {
      final List<int>? bytes =
          file.bytes ??
          (file.path == null ? null : await XFile(file.path!).readAsBytes());
      if (bytes != null) {
        base64Value = base64Encode(bytes);
      }
    }

    serialized.add(<String, dynamic>{
      'name': file.name,
      'uri': file.path == null ? null : Uri.file(file.path!).toString(),
      'mimeType': mimeType,
      'size': file.size,
      if (responseType == 'base64') 'base64': base64Value,
      if (includeDataUrl && base64Value != null)
        'dataUrl': 'data:$mimeType;base64,$base64Value',
    });
  }
  return serialized;
}

List<String> _xFilesToUriStrings(List<XFile> files) {
  return files.map((file) => Uri.file(file.path).toString()).toList();
}

List<String> _acceptTypesFromValue(Object? value) {
  final rawTypes = switch (value) {
    final String stringValue => stringValue.split(','),
    final Iterable<dynamic> iterableValue => iterableValue.expand((item) {
      final stringValue = item.toString();
      return stringValue.contains(',')
          ? stringValue.split(',')
          : <String>[stringValue];
    }),
    _ => const <String>[],
  };

  return rawTypes
      .map((type) => type.trim().toLowerCase())
      .where((type) => type.isNotEmpty)
      .toList();
}

bool _acceptsImages(List<String> acceptTypes) {
  return acceptTypes
      .map((type) => type.trim())
      .where((type) => type.isNotEmpty)
      .any(_isImageAcceptType);
}

bool _acceptsOnlyImages(List<String> acceptTypes) {
  final normalizedTypes = acceptTypes
      .map((type) => type.trim())
      .where((type) => type.isNotEmpty)
      .toList();
  if (normalizedTypes.isEmpty) {
    return false;
  }
  return normalizedTypes.every(_isImageAcceptType);
}

bool _isImageAcceptType(String acceptType) {
  final value = acceptType.toLowerCase();
  return value.startsWith('image/') ||
      _imageFileExtensions.map((extension) => '.$extension').contains(value);
}

bool _acceptsVideos(List<String> acceptTypes) {
  return acceptTypes
      .map((type) => type.trim())
      .where((type) => type.isNotEmpty)
      .any(_isVideoAcceptType);
}

bool _acceptsOnlyVideos(List<String> acceptTypes) {
  final normalizedTypes = acceptTypes
      .map((type) => type.trim())
      .where((type) => type.isNotEmpty)
      .toList();
  if (normalizedTypes.isEmpty) {
    return false;
  }
  return normalizedTypes.every(_isVideoAcceptType);
}

bool _isVideoAcceptType(String acceptType) {
  final value = acceptType.toLowerCase();
  return value.startsWith('video/') ||
      _videoFileExtensions.map((extension) => '.$extension').contains(value);
}

bool _acceptsOnlyMedia(List<String> acceptTypes) {
  final normalizedTypes = acceptTypes
      .map((type) => type.trim())
      .where((type) => type.isNotEmpty)
      .toList();
  if (normalizedTypes.isEmpty) {
    return false;
  }
  return normalizedTypes.every(
    (type) => _isImageAcceptType(type) || _isVideoAcceptType(type),
  );
}

List<String> _allowedExtensionsForAcceptTypes(List<String> acceptTypes) {
  final extensions = <String>{};
  for (final acceptType in acceptTypes) {
    extensions.addAll(_extensionsForAcceptType(acceptType));
  }
  return extensions.toList()..sort();
}

List<String> _extensionsForAcceptType(String acceptType) {
  final value = acceptType.trim().toLowerCase();
  if (value.isEmpty) {
    return const <String>[];
  }
  if (value.startsWith('.')) {
    return <String>[value.substring(1)];
  }
  return switch (value) {
    'image/*' => _imageFileExtensions.toList(),
    'video/*' => _videoFileExtensions.toList(),
    'image/png' => const <String>['png'],
    'image/jpeg' => const <String>['jpg', 'jpeg'],
    'image/webp' => const <String>['webp'],
    'image/gif' => const <String>['gif'],
    'image/bmp' => const <String>['bmp'],
    'image/heic' => const <String>['heic'],
    'image/heif' => const <String>['heif'],
    'video/mp4' => const <String>['mp4'],
    'video/quicktime' => const <String>['mov'],
    'video/x-msvideo' => const <String>['avi'],
    'video/x-ms-wmv' => const <String>['wmv'],
    'video/x-matroska' => const <String>['mkv'],
    'video/x-m4v' => const <String>['m4v'],
    'video/webm' => const <String>['webm'],
    'video/3gpp' => const <String>['3gp'],
    'application/pdf' => const <String>['pdf'],
    'text/plain' => const <String>['txt'],
    'application/vnd.android.package-archive' => const <String>['apk'],
    _ => const <String>[],
  };
}

({FileType type, List<String>? allowedExtensions})
_filePickerFilterForAcceptTypes(
  List<String> acceptTypes,
) {
  final normalizedTypes = _acceptTypesFromValue(acceptTypes);
  if (normalizedTypes.isEmpty) {
    return (type: FileType.any, allowedExtensions: null);
  }
  if (!_shouldUseMediaPickerForFileSelection(normalizedTypes)) {
    final allowedExtensions = _allowedExtensionsForAcceptTypes(normalizedTypes);
    if (allowedExtensions.isNotEmpty) {
      return (type: FileType.custom, allowedExtensions: allowedExtensions);
    }
    return (type: FileType.any, allowedExtensions: null);
  }
  if (_acceptsOnlyImages(normalizedTypes)) {
    return (type: FileType.image, allowedExtensions: null);
  }
  if (_acceptsOnlyVideos(normalizedTypes)) {
    return (type: FileType.video, allowedExtensions: null);
  }
  if (_acceptsOnlyMedia(normalizedTypes)) {
    return (type: FileType.media, allowedExtensions: null);
  }

  return (type: FileType.any, allowedExtensions: null);
}

bool _shouldUseMediaPickerForFileSelection(List<String> acceptTypes) {
  final normalizedTypes = _acceptTypesFromValue(acceptTypes);
  if (normalizedTypes.isEmpty) {
    return false;
  }
  return normalizedTypes.every(
    (type) => type == 'image/*' || type == 'video/*',
  );
}

String _guessMimeType(String fileName) {
  final lower = fileName.toLowerCase();
  if (lower.endsWith('.png')) {
    return 'image/png';
  }
  if (lower.endsWith('.jpg') || lower.endsWith('.jpeg')) {
    return 'image/jpeg';
  }
  if (lower.endsWith('.webp')) {
    return 'image/webp';
  }
  if (lower.endsWith('.gif')) {
    return 'image/gif';
  }
  if (lower.endsWith('.bmp')) {
    return 'image/bmp';
  }
  if (lower.endsWith('.heic')) {
    return 'image/heic';
  }
  if (lower.endsWith('.heif')) {
    return 'image/heif';
  }
  if (lower.endsWith('.mp4')) {
    return 'video/mp4';
  }
  if (lower.endsWith('.mov')) {
    return 'video/quicktime';
  }
  if (lower.endsWith('.avi')) {
    return 'video/x-msvideo';
  }
  if (lower.endsWith('.wmv')) {
    return 'video/x-ms-wmv';
  }
  if (lower.endsWith('.mkv')) {
    return 'video/x-matroska';
  }
  if (lower.endsWith('.m4v')) {
    return 'video/x-m4v';
  }
  if (lower.endsWith('.webm')) {
    return 'video/webm';
  }
  if (lower.endsWith('.3gp')) {
    return 'video/3gpp';
  }
  if (lower.endsWith('.pdf')) {
    return 'application/pdf';
  }
  if (lower.endsWith('.txt')) {
    return 'text/plain';
  }
  if (lower.endsWith('.apk')) {
    return 'application/vnd.android.package-archive';
  }
  return 'application/octet-stream';
}

Future<void> _showWebAlert(WebViewController controller, String message) async {
  try {
    await controller.runJavaScript('window.alert(${jsonEncode(message)});');
  } on Object catch (error, stackTrace) {
    debugPrint('展示网页弹窗失败：$error\n$stackTrace');
  }
}

Future<List<XFile>?> _pickAcceptedMediaFiles(
  ImagePicker imagePicker, {
  required List<String> acceptTypes,
  required bool capture,
  required bool multiple,
  WebViewController? controller,
}) async {
  if (!_shouldUseMediaPickerForFileSelection(acceptTypes)) {
    return null;
  }

  if (capture) {
    if (_acceptsOnlyImages(acceptTypes)) {
      return <XFile>[
        if (await _pickCameraImage(
              imagePicker,
              showPermissionAlert: true,
              preserveOriginal: true,
              controller: controller,
            )
            case final XFile file)
          file,
      ];
    }
    if (_acceptsOnlyVideos(acceptTypes)) {
      return <XFile>[
        if (await _pickCameraVideo(
              imagePicker,
              showPermissionAlert: true,
              controller: controller,
            )
            case final XFile file)
          file,
      ];
    }
    return null;
  }

  if (_acceptsOnlyImages(acceptTypes)) {
    return _pickGalleryImages(
      imagePicker,
      multiple: multiple,
      preserveOriginal: true,
    );
  }
  if (_acceptsOnlyVideos(acceptTypes)) {
    return _pickGalleryVideos(
      imagePicker,
      multiple: multiple,
    );
  }
  if (_acceptsOnlyMedia(acceptTypes)) {
    return _pickGalleryMedia(
      imagePicker,
      multiple: multiple,
      preserveOriginal: true,
    );
  }
  return null;
}

Future<Object?> _pickFilesFromBridgePayload(
  Map<String, dynamic> payload, {
  required ImagePicker imagePicker,
  WebViewController? controller,
}) async {
  final multiple = _boolValue(payload['multiple']);
  final responseType = (payload['responseType'] ?? 'uri').toString();
  final includeBinary = responseType == 'dataUrl' || responseType == 'base64';
  final acceptTypes = _acceptTypesFromValue(
    payload['acceptTypes'] ?? payload['accept'],
  );
  final capture = _boolValue(payload['capture']);

  final mediaFiles = await _pickAcceptedMediaFiles(
    imagePicker,
    acceptTypes: acceptTypes,
    capture: capture,
    multiple: multiple,
    controller: controller,
  );
  if (mediaFiles != null) {
    final serialized = await _serializeXFiles(
      mediaFiles,
      responseType: responseType,
    );
    return multiple ? serialized : serialized.firstOrNull;
  }

  final pickerFilter = _filePickerFilterForAcceptTypes(acceptTypes);
  final result = await FilePicker.platform.pickFiles(
    allowMultiple: multiple,
    type: pickerFilter.type,
    allowedExtensions: pickerFilter.allowedExtensions,
    withData: includeBinary,
  );

  if (result == null) {
    return multiple ? <Map<String, dynamic>>[] : null;
  }

  final serialized = await _serializePlatformFiles(
    result.files,
    responseType: responseType,
  );
  return multiple ? serialized : serialized.firstOrNull;
}

Future<List<String>> _selectFilesForWebView(
  FileSelectorParams params, {
  required ImagePicker imagePicker,
  WebViewController? controller,
}) async {
  if (params.mode == FileSelectorMode.save) {
    return <String>[];
  }

  final acceptTypes = _acceptTypesFromValue(params.acceptTypes);
  final multiple = params.mode == FileSelectorMode.openMultiple;

  final mediaFiles = await _pickAcceptedMediaFiles(
    imagePicker,
    acceptTypes: acceptTypes,
    capture: params.isCaptureEnabled,
    multiple: multiple,
    controller: controller,
  );
  if (mediaFiles != null) {
    return _xFilesToUriStrings(mediaFiles);
  }

  final pickerFilter = _filePickerFilterForAcceptTypes(acceptTypes);
  final result = await FilePicker.platform.pickFiles(
    allowMultiple: multiple,
    type: pickerFilter.type,
    allowedExtensions: pickerFilter.allowedExtensions,
  );

  if (result == null) {
    return <String>[];
  }

  return result.files
      .map((file) => file.path)
      .whereType<String>()
      .map((path) => Uri.file(path).toString())
      .toList();
}

bool _boolValue(Object? value, {bool defaultValue = false}) {
  return switch (value) {
    final bool boolValue => boolValue,
    final String stringValue => stringValue.toLowerCase() == 'true',
    final int intValue => intValue != 0,
    _ => defaultValue,
  };
}
