import 'dart:convert';
import 'dart:typed_data';

import 'package:proxypin/network/http/http.dart';

import '../network/components/manager/request_crypto_manager.dart';
import '../network/util/logger.dart';
import 'aes.dart';

class CryptoDecodedResult {
  final Uint8List bytes;
  final String? text;
  final CryptoRule? rule;

  const CryptoDecodedResult({required this.bytes, this.text, this.rule});

  bool get hasText => text != null && text!.trim().isNotEmpty;
}

class CryptoBodyDecoder {
  static Future<CryptoDecodedResult?> maybeDecode(HttpMessage message) async {
    final ruleStore = await RequestCryptoManager.instance;

    CryptoRule? match = ruleStore.getMatchingRule(message);
    if (match == null) {
      return null;
    }

    return _tryDecode(message, match.config, rule: match);
  }

  static CryptoDecodedResult? decode(HttpMessage message, CryptoKeyConfig config) {
    return _tryDecode(message, config);
  }

  static CryptoDecodedResult? decodeWithConfig(HttpMessage message, CryptoKeyConfig config) {
    return _tryDecode(message, config);
  }

  static CryptoDecodedResult? _tryDecode(HttpMessage message, CryptoKeyConfig config, {CryptoRule? rule}) {
    final raw = message.body;
    if (raw == null || raw.isEmpty || !config.isReady) {
      return null;
    }

    // If rule specifies a field, try to parse body as JSON and extract that field for decryption
    final fieldPath = rule?.field?.trim();
    logger.d("CryptoBodyDecoder _tryDecode with config: $config and rule: $rule fieldPath: $fieldPath");
    if (fieldPath != null && fieldPath.isNotEmpty) {
      // parse body as text
      final content = _bytesToString(raw, message.charset);
      if (content == null) return null;
      dynamic jsonObj;
      try {
        jsonObj = jsonDecode(content);
      } catch (_) {
        return null;
      }

      final extracted = _extractJsonField(jsonObj, fieldPath);
      if (extracted == null) return null;
      // Only attempt when extracted is a string or number (we stringify otherwise)
      String fieldStr = extracted.toString();

      // build candidates from the field string: raw bytes and base64-decoded (if looks like base64)
      final candidates = <Uint8List>[];
      final base64Candidate = _tryDecodeBase64String(fieldStr);
      if (base64Candidate != null) candidates.add(base64Candidate);

      for (final candidate in candidates) {
        try {
          final decrypted = _decryptCandidate(candidate, config);
          // print("CryptoBodyDecoder _tryDecode decrypted bytes: $decrypted");
          if (decrypted != null) {
            return CryptoDecodedResult(bytes: decrypted, text: _bytesToString(decrypted, message.charset), rule: rule);
          }
        } catch (e) {
          logger.d("CryptoBodyDecoder _tryDecode decryption error: $e");
          continue;
        }
      }
      return null;
    }

    // whole-body: try raw bytes and base64-decoded text
    final candidates = <Uint8List>[];
    // candidates.add(Uint8List.fromList(raw));
    final base64Candidate = _fromBase64(raw);
    if (base64Candidate != null) {
      candidates.add(base64Candidate);
    }
    // logger.d("CryptoBodyDecoder _tryDecode total candidates: ${candidates.length}");
    for (final candidate in candidates) {
      try {
        final decrypted = _decryptCandidate(candidate, config);
        // logger.d("CryptoBodyDecoder _tryDecode decrypted bytes: $decrypted");
        if (decrypted != null) {
          return CryptoDecodedResult(bytes: decrypted, text: _bytesToString(decrypted, message.charset), rule: rule);
        }
      } catch (e) {
        logger.d("CryptoBodyDecoder _tryDecode decryption error: $e");
        continue;
      }
    }
    return null;
  }

  // Attempt to decrypt a single candidate, handling ivSource == 'prefix' by extracting IV bytes.
  static Uint8List? _decryptCandidate(Uint8List candidate, CryptoKeyConfig config) {
    const int aesBlockSize = 16;
    // If using prefix-mode, split IV and cipher bytes and ensure cipher bytes length is valid for non-PKCS7 paddings
    if (config.mode == 'CBC' && config.ivSource == 'prefix') {
      final n = config.ivPrefixLength;
      if (candidate.length <= n) return null;
      final ivBytes = candidate.sublist(0, n);
      final cipherBytes = candidate.sublist(n);
      // For non-PKCS7 paddings (e.g., ZeroPadding/raw) the cipher bytes length must be multiple of block size
      if (config.padding != 'PKCS7' && (cipherBytes.length % aesBlockSize != 0)) return null;
      final ivStr = 'base64:' + base64.encode(ivBytes);
      try {
        return AesUtils.decrypt(cipherBytes,
            key: config.key, keyLength: config.keyLength, mode: config.mode, padding: config.padding, iv: ivStr);
      } catch (e) {
        logger.d('CryptoBodyDecoder _decryptCandidate error (prefix): $e');
        return null;
      }
    } else {
      // iv provided in config.iv (may include base64: prefix or be plain text)
      // For non-PKCS7 paddings ensure candidate length is block-aligned before attempting raw decrypt
      if (config.padding != 'PKCS7' && (candidate.length % aesBlockSize != 0)) return null;
      final ivParam = (config.mode == 'CBC') ? config.iv : null;
      try {
        return AesUtils.decrypt(candidate,
            key: config.key, keyLength: config.keyLength, mode: config.mode, padding: config.padding, iv: ivParam);
      } catch (e) {
        logger.d('CryptoBodyDecoder _decryptCandidate error: $e');
        return null;
      }
    }
  }

  // Try to decode a base64 string; return bytes or null
  static Uint8List? _tryDecodeBase64String(String s) {
    final trimmed = s.trim();
    if (trimmed.isEmpty) return null;
    if (!_maybeBase64(trimmed)) return null;
    try {
      return Uint8List.fromList(base64.decode(trimmed));
    } catch (_) {
      return null;
    }
  }

  // Extract a nested JSON field by a dot-separated path. Supports array indexes like items[0].value
  static dynamic _extractJsonField(dynamic jsonObj, String path) {
    final parts = path.split('.');
    dynamic current = jsonObj;
    for (final part in parts) {
      if (current == null) return null;
      // check for array index like key[index]
      final arrayMatch = RegExp(r"^([a-zA-Z0-9_\-]+)\[(\d+)\]").firstMatch(part);
      if (arrayMatch != null) {
        final key = arrayMatch.group(1)!;
        final idx = int.parse(arrayMatch.group(2)!);
        if (current is Map && current.containsKey(key)) {
          final list = current[key];
          if (list is List && idx >= 0 && idx < list.length) {
            current = list[idx];
            continue;
          }
          return null;
        }
        return null;
      }

      // normal key or numeric index for lists
      if (current is Map) {
        if (!current.containsKey(part)) return null;
        current = current[part];
      } else if (current is List) {
        final idx = int.tryParse(part);
        if (idx == null || idx < 0 || idx >= current.length) return null;
        current = current[idx];
      } else {
        return null;
      }
    }
    return current;
  }

  static Uint8List? _fromBase64(List<int> raw) {
    try {
      final content = utf8.decode(raw).trim();
      if (content.isEmpty || !_maybeBase64(content)) {
        return null;
      }
      return Uint8List.fromList(base64.decode(content));
    } catch (_) {
      return null;
    }
  }

  static bool _maybeBase64(String value) {
    if (value.length % 4 != 0) return false;
    if (value.contains(RegExp(r'[^A-Za-z0-9+/=\r\n]'))) return false;
    return true;
  }

  static String? _bytesToString(List<int> bytes, String? charset) {
    try {
      if (charset == null || charset.toLowerCase().contains('utf')) {
        return utf8.decode(bytes);
      }
      return const Latin1Codec().decode(bytes);
    } catch (_) {
      try {
        return utf8.decode(bytes);
      } catch (_) {
        return null;
      }
    }
  }
}
