import 'dart:io';
import 'dart:isolate';
import 'dart:math';

import 'package:clock/clock.dart';
import 'package:convert/convert.dart';
import 'package:crypto/crypto.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:gal/gal.dart';
import 'package:image/image.dart' as img;
import 'package:intl/intl.dart';
import 'package:local_auth/local_auth.dart';
import 'package:path/path.dart';
import 'package:provider/provider.dart';
import 'package:twonly/src/localization/generated/app_localizations.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/providers/settings.provider.dart';
import 'package:twonly/src/services/backup.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';

extension ShortCutsExtension on BuildContext {
  AppLocalizations get lang => AppLocalizations.of(this)!;
  ColorScheme get color => Theme.of(this).colorScheme;
  Future<dynamic> navPush(Widget route) async {
    return Navigator.push(
      this,
      MaterialPageRoute(
        builder: (context) {
          return route;
        },
      ),
    );
  }
}

Future<String?> saveImageToGallery(
  Uint8List imageBytes, {
  DateTime? createdAt,
  String? name,
}) async {
  var bytesToProcess = imageBytes;

  if (createdAt != null) {
    try {
      bytesToProcess = await Isolate.run(() {
        final image = img.decodeImage(imageBytes);
        if (image != null) {
          final formattedDate = DateFormat(
            'yyyy:MM:dd HH:mm:ss',
          ).format(createdAt);
          image.exif.imageIfd[0x0132] = img.IfdValueAscii(
            formattedDate,
          ); // DateTime
          image.exif.exifIfd[0x9003] = img.IfdValueAscii(
            formattedDate,
          ); // DateTimeOriginal
          image.exif.exifIfd[0x9004] = img.IfdValueAscii(
            formattedDate,
          ); // DateTimeDigitized

          return img.encodeJpg(image);
        }
        return imageBytes;
      });
    } catch (e) {
      Log.error(e);
    }
  }

  final hasAccess = await Gal.hasAccess(toAlbum: true);
  if (!hasAccess) {
    await Gal.requestAccess(toAlbum: true);
  }
  try {
    await Gal.putImageBytes(
      bytesToProcess,
      album: 'twonly',
      name: name ?? 'image',
    );
    return null;
  } on GalException catch (e) {
    Log.error(e);
    return e.type.message;
  }
}

Future<String?> saveVideoToGallery(
  String videoPath, {
  String? name,
}) async {
  final hasAccess = await Gal.hasAccess(toAlbum: true);
  if (!hasAccess) {
    await Gal.requestAccess(toAlbum: true);
  }

  var pathToSave = videoPath;
  File? tempFile;

  try {
    if (name != null) {
      final file = File(videoPath);
      final extension = file.path.split('.').last;
      final tempDir = Directory.systemTemp;
      tempFile = File(join(tempDir.path, '$name.$extension'));
      if (tempFile.existsSync()) {
        try {
          tempFile.deleteSync();
        } catch (_) {}
      }
      file.copySync(tempFile.path);
      pathToSave = tempFile.path;
    }
    await Gal.putVideo(pathToSave, album: 'twonly');
    return null;
  } on GalException catch (e) {
    Log.error(e);
    return e.type.message;
  } finally {
    if (tempFile != null && tempFile.existsSync()) {
      try {
        tempFile.deleteSync();
      } catch (e) {
        Log.error('Failed to delete temp video file: $e');
      }
    }
  }
}

Uint8List getRandomUint8List(int length) {
  final random = Random.secure();
  final randomBytes = Uint8List(length);

  for (var i = 0; i < length; i++) {
    randomBytes[i] = random.nextInt(256); // Generate a random byte (0-255)
  }

  return randomBytes;
}

const _chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890';
Random _rnd = Random();

String getRandomString(int length) => String.fromCharCodes(
  Iterable.generate(
    length,
    (_) => _chars.codeUnitAt(_rnd.nextInt(_chars.length)),
  ),
);

String errorCodeToText(BuildContext context, ErrorCode code) {
  // ignore: exhaustive_cases
  switch (code) {
    case ErrorCode.InternalError:
      return context.lang.errorInternalError;
    case ErrorCode.InvalidInvitationCode:
      return context.lang.errorInvalidInvitationCode;
    case ErrorCode.UsernameAlreadyTaken:
      return context.lang.errorUsernameAlreadyTaken;
    case ErrorCode.UsernameNotValid:
      return context.lang.errorUsernameNotValid;
    case ErrorCode.NotEnoughCredit:
      return context.lang.errorNotEnoughCredit;
    case ErrorCode.PlanLimitReached:
      return context.lang.errorPlanLimitReached;
    case ErrorCode.PlanNotAllowed:
      return context.lang.errorPlanNotAllowed;
    case ErrorCode.VoucherInValid:
      return context.lang.errorVoucherInvalid;
    case ErrorCode.PlanUpgradeNotYearly:
      return context.lang.errorPlanUpgradeNotYearly;
  }
  return code.toString();
}

String formatDuration(BuildContext context, int seconds) {
  if (seconds < 60) {
    return '$seconds ${context.lang.durationShortSecond}';
  } else if (seconds < 3600) {
    final minutes = seconds ~/ 60;
    return '$minutes ${context.lang.durationShortMinute}';
  } else if (seconds < 86400) {
    final hours = seconds ~/ 3600;
    return '$hours ${context.lang.durationShortHour}';
  } else {
    final days = seconds ~/ 86400;
    return context.lang.durationShortDays(days);
  }
}

Future<bool> authenticateUser(
  String localizedReason, {
  bool force = true,
}) async {
  try {
    final auth = LocalAuthentication();
    final didAuthenticate = await auth.authenticate(
      localizedReason: localizedReason,
    );
    if (didAuthenticate) {
      return true;
    }
  } on LocalAuthException catch (e) {
    Log.error(e.toString());
    if (e.code == LocalAuthExceptionCode.noBiometricHardware ||
        e.code == LocalAuthExceptionCode.noBiometricsEnrolled ||
        e.code == LocalAuthExceptionCode.noCredentialsSet) {
      if (!force) {
        return true;
      }
    }
  } catch (e) {
    // Catch unexpected platform exceptions (e.g. PlatformException) that
    // would otherwise propagate and leave callers in a broken state.
    Log.error('Unexpected authentication error: $e');
  }
  return false;
}

bool isDarkMode(BuildContext context) {
  final selectedTheme = context.read<SettingsChangeProvider>().themeMode;

  final isDarkMode =
      MediaQuery.of(context).platformBrightness == Brightness.dark;

  return selectedTheme == ThemeMode.dark ||
      (selectedTheme == ThemeMode.system && isDarkMode);
}

bool isToday(DateTime lastImageSend) {
  final now = clock.now();
  return lastImageSend.year == now.year &&
      lastImageSend.month == now.month &&
      lastImageSend.day == now.day;
}

String truncateString(String input, {int maxLength = 20}) {
  if (input.length > maxLength) {
    return '${input.characters.take(maxLength)}...';
  }
  return input;
}

String formatDateTime(BuildContext context, DateTime? dateTime) {
  if (dateTime == null) {
    return 'Never';
  }
  final now = clock.now();
  final difference = now.difference(dateTime);

  final date = DateFormat.yMd(
    Localizations.localeOf(context).toLanguageTag(),
  ).format(dateTime);

  final time = DateFormat.Hm(
    Localizations.localeOf(context).toLanguageTag(),
  ).format(dateTime);

  if (difference.inDays == 0) {
    return time;
  } else {
    return '$time $date';
  }
}

String formatRelativeDateTime(BuildContext context, DateTime? dateTime) {
  if (dateTime == null) {
    return '-';
  }
  final now = clock.now();
  final today = DateTime(now.year, now.month, now.day);
  final yesterday = today.subtract(const Duration(days: 1));
  final dateDay = DateTime(dateTime.year, dateTime.month, dateTime.day);

  final time = DateFormat.Hm(
    Localizations.localeOf(context).toLanguageTag(),
  ).format(dateTime);

  if (dateDay == today) {
    return context.lang.todayAt(time);
  } else if (dateDay == yesterday) {
    return context.lang.yesterdayAt(time);
  } else {
    return formatDateTime(context, dateTime);
  }
}

String formatBytes(int bytes) {
  if (bytes <= 0) return '0 Bytes';
  const units = <String>['Bytes', 'KB', 'MB', 'GB', 'TB'];
  final unitIndex = (log(bytes) / log(1000)).floor();
  final formattedSize = bytes / pow(1000, unitIndex);
  return '${formattedSize.ceil()} ${units[unitIndex]}';
}

bool isUUIDNewer(String uuid1, String uuid2) {
  try {
    final timestamp1 = int.parse(uuid1.substring(0, 8), radix: 16);
    final timestamp2 = int.parse(uuid2.substring(0, 8), radix: 16);
    return timestamp1 > timestamp2;
  } catch (e) {
    return false;
  }
}

String uint8ListToHex(List<int> bytes) {
  return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join();
}

Uint8List hexToUint8List(String hex) => Uint8List.fromList(
  List<int>.generate(
    hex.length ~/ 2,
    (i) => int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16),
  ),
);

String getUUIDforDirectChat(int a, int b) {
  if (a < 0 || b < 0) {
    throw ArgumentError('Inputs must be non-negative integers.');
  }

  // Mask to 64 bits in case inputs exceed 64 bits
  final mask64 = (BigInt.one << 64) - BigInt.one;
  final ai = BigInt.from(a) & mask64;
  final bi = BigInt.from(b) & mask64;

  // Ensure the bigger integer is in front (high 64 bits)
  final hi = ai >= bi ? ai : bi;
  final lo = ai >= bi ? bi : ai;

  final combined = (hi << 64) | lo;

  final hex = combined.toRadixString(16).padLeft(32, '0');

  final parts = [
    hex.substring(0, 8),
    hex.substring(8, 12),
    hex.substring(12, 16),
    hex.substring(16, 20),
    hex.substring(20, 32),
  ];
  return parts.join('-');
}

String friendlyDateTime(
  BuildContext context,
  DateTime dt, {
  bool includeSeconds = false,
  Locale? locale,
}) {
  // Build date part
  final datePart = DateFormat.yMd(
    Localizations.localeOf(context).toString(),
  ).format(dt);

  final use24Hour = MediaQuery.of(context).alwaysUse24HourFormat;

  var timePart = '';
  if (use24Hour) {
    timePart = DateFormat.jm(
      Localizations.localeOf(context).toString(),
    ).format(dt);
  } else {
    timePart = DateFormat.Hm(
      Localizations.localeOf(context).toString(),
    ).format(dt);
  }

  return '$timePart $datePart';
}

Future<List<int>> sha256File(File file) async {
  final input = file.openRead();
  final sha256Sink = AccumulatorSink<Digest>();
  final converter = sha256.startChunkedConversion(sha256Sink);
  await input.forEach(converter.add);
  converter.close();
  return sha256Sink.events.single.bytes;
}

List<TextSpan> formattedText(
  BuildContext context,
  String input, {
  Color? textColor,
  Color? boldTextColor,
}) {
  final defaultColor = textColor ?? Theme.of(context).colorScheme.onSurface;

  final regex = RegExp(r'\*(.*?)\*');
  final spans = <TextSpan>[];

  var lastMatchEnd = 0;

  for (final match in regex.allMatches(input)) {
    if (match.start > lastMatchEnd) {
      spans.add(
        TextSpan(
          text: input.substring(lastMatchEnd, match.start),
          style: TextStyle(color: defaultColor),
        ),
      );
    }

    spans.add(
      TextSpan(
        text: match.group(1),
        style: TextStyle(
          fontWeight: FontWeight.bold,
          color: boldTextColor ?? defaultColor,
        ),
      ),
    );

    lastMatchEnd = match.end;
  }

  if (lastMatchEnd < input.length) {
    spans.add(
      TextSpan(
        text: input.substring(lastMatchEnd),
        style: TextStyle(color: defaultColor),
      ),
    );
  }

  return spans;
}

String joinWithAnd(List<String> items, String andWord) {
  if (items.isEmpty) return '';
  if (items.length == 1) return items.first;
  return '${items.sublist(0, items.length - 1).join(', ')} $andWord ${items.last}';
}

String createEmailHint(String email) {
  final parts = email.split('@');
  if (parts.length != 2) return email;

  final local = parts[0];
  final domain = parts[1];

  final localMasked = local.length > 2
      ? '${local[0]}${'*' * (local.length - 2)}${local[local.length - 1]}'
      : local;

  final domainParts = domain.split('.');
  if (domainParts.isEmpty) return '$localMasked@$domain';

  final domainName = domainParts[0];
  final domainNameMasked = domainName.length > 1
      ? '${domainName[0]}${'*' * (domainName.length - 1)}'
      : domainName;

  final restDomain = domainParts.skip(1).join('.');
  return '$localMasked@$domainNameMasked${restDomain.isNotEmpty ? '.$restDomain' : ''}';
}

extension RecoveryErrorLocalization on RecoveryError {
  String toLocalizedString(BuildContext context) {
    switch (this) {
      case RecoveryError.noInternet:
        return context.lang.recoverErrorNoInternet;
      case RecoveryError.usernameNotValid:
        return context.lang.recoverErrorUsernameNotValid;
      case RecoveryError.passwordInvalid:
        return context.lang.recoverErrorPasswordInvalid;
      case RecoveryError.tryAgainLater:
        return context.lang.recoverErrorTryAgainLater;
      case RecoveryError.unkownError:
        return context.lang.recoverErrorUnknown;
    }
  }
}
