import 'dart:math' as math;

import 'package:flutter/foundation.dart' show kIsWeb, compute;
import 'package:flutter_sms_inbox/flutter_sms_inbox.dart';
import '../models/subscription_model.dart';
import '../utils/logger.dart';
import '../temp/test_sms_data.dart';
import '../services/subscription_url_matcher.dart';
import '../utils/platform_helper.dart';
import '../utils/business_day_util.dart';
import '../services/sms_scan/sms_scan_result.dart';
import '../models/payment_card_suggestion.dart';
import '../l10n/app_localizations.dart';
import '../navigator_key.dart';

class SmsScanner {
  final SmsQuery _query = SmsQuery();

  Future<List<SmsScanResult>> scanForSubscriptions() async {
    try {
      List<dynamic> smsList;
      Log.d('SmsScanner: 스캔 시작');

      // 플랫폼별 분기 처리
      if (kIsWeb) {
        // 웹 환경: 테스트 데이터 사용
        Log.i('SmsScanner: 웹 환경에서 테스트 데이터 사용');
        smsList = TestSmsData.getTestData();
        Log.d('SmsScanner: 테스트 데이터 개수: ${smsList.length}');
      } else if (PlatformHelper.isIOS) {
        // iOS: SMS 접근 불가, 빈 리스트 반환
        Log.w('SmsScanner: iOS에서는 SMS 스캔 불가');
        return [];
      } else if (PlatformHelper.isAndroid) {
        // Android: flutter_sms_inbox 사용
        Log.i('SmsScanner: Android에서 실제 SMS 스캔');
        smsList = await _scanAndroidSms();
        Log.d('SmsScanner: 스캔된 SMS 개수: ${smsList.length}');
      } else {
        // 기타 플랫폼
        Log.w('SmsScanner: 지원하지 않는 플랫폼');
        return [];
      }

      final filteredSms = smsList
          .whereType<Map<String, dynamic>>()
          .where(_isEligibleSubscriptionSms)
          .toList();

      Log.d(
          'SmsScanner: 유효 결제 SMS ${filteredSms.length}건 / 전체 ${smsList.length}건');

      if (filteredSms.isEmpty) {
        Log.w('SmsScanner: 결제 패턴 SMS 미검출');
        return [];
      }

      // SMS 데이터를 분석하여 반복 결제되는 구독 식별
      final List<SmsScanResult> subscriptions = [];
      final serviceGroups = _groupMessagesByIdentifier(filteredSms);

      Log.d('SmsScanner: 그룹화된 키 수: ${serviceGroups.length}');

      for (final entry in serviceGroups.entries) {
        Log.d('SmsScanner: 그룹 "${entry.key}" - 메시지 ${entry.value.length}건');
        final repeatResult = _detectRepeatingSubscriptions(entry.value);
        if (repeatResult == null) {
          Log.d('SmsScanner: 반복 조건 불충족 - ${entry.key}');
          continue;
        }

        final result =
            _parseSms(repeatResult.baseMessage, repeatResult.repeatCount);
        if (result != null) {
          Log.i(
              'SmsScanner: 구독 추가: ${result.model.serviceName}, 반복 횟수: ${result.model.repeatCount}');
          subscriptions.add(result);
        } else {
          Log.w('SmsScanner: 구독 파싱 실패: ${entry.key}');
        }
      }

      Log.d('SmsScanner: 최종 구독 개수: ${subscriptions.length}');
      return subscriptions;
    } catch (e) {
      Log.e('SmsScanner: 예외 발생', e);
      final loc = _loc();
      throw Exception(loc?.smsScanErrorWithMessage(e.toString()) ??
          'Error occurred during SMS scan: $e');
    }
  }

  // Android에서 flutter_sms_inbox를 사용한 SMS 스캔
  Future<List<dynamic>> _scanAndroidSms() async {
    try {
      final messages = await _query.getAllSms;

      // Isolate로 넘길 수 있도록 직렬화 (plugin 객체 직접 전달 불가)
      final serialized = <Map<String, dynamic>>[];
      for (final message in messages) {
        serialized.add({
          'body': message.body ?? '',
          'address': message.address ?? '',
          'dateMillis': (message.date ?? DateTime.now()).millisecondsSinceEpoch,
        });
      }

      // 대량 파싱은 별도 Isolate로 오프로딩
      final List<Map<String, dynamic>> smsList =
          await compute(_parseRawSmsBatch, serialized);

      return smsList;
    } catch (e) {
      Log.e('SmsScanner: Android SMS 스캔 실패', e);
      return [];
    }
  }

  // (사용되지 않음) 기존 단일 메시지 파서 제거됨 - Isolate 배치 파싱으로 대체

  SmsScanResult? _parseSms(Map<String, dynamic> sms, int repeatCount) {
    try {
      final loc = _loc();
      final unknownLabel = loc?.unknownService ?? 'Unknown service';
      final serviceNameRaw = sms['serviceName'] as String?;
      final serviceName =
          (serviceNameRaw == null || serviceNameRaw.trim().isEmpty)
              ? unknownLabel
              : serviceNameRaw;
      final monthlyCost = (sms['monthlyCost'] as num?)?.toDouble() ?? 0.0;
      final billingCycle = SubscriptionModel.normalizeBillingCycle(
          sms['billingCycle'] as String? ?? 'monthly');
      final nextBillingDateStr = sms['nextBillingDate'] as String?;
      // 실제 반복 횟수 사용 (테스트 데이터에서는 이미 제공됨)
      final actualRepeatCount = repeatCount > 0 ? repeatCount : 1;
      final message = sms['message'] as String? ?? '';

      // 통화 단위 감지 - 메시지 내용과 서비스명 모두 검사
      String currency = _detectCurrency(message);

      // 서비스명에 따라 통화 단위 재확인
      final dollarServices = [
        'GitHub',
        'GitHub Pro',
        'Netflix US',
        'Spotify',
        'Spotify Premium'
      ];
      if (dollarServices.any((service) => serviceName.contains(service))) {
        Log.d('서비스명 $serviceName으로 USD 통화 단위 확정');
        currency = 'USD';
      }

      DateTime? nextBillingDate;
      // 외부에서 계산된 다음 결제일이 있으면 우선 사용
      final overrideNext = sms['overrideNextBillingDate'] as String?;
      if (overrideNext != null) {
        nextBillingDate = DateTime.tryParse(overrideNext);
      } else if (nextBillingDateStr != null) {
        nextBillingDate = DateTime.tryParse(nextBillingDateStr);
      }

      DateTime? lastPaymentDate;
      final previousPaymentDateStr = sms['previousPaymentDate'] as String?;
      if (previousPaymentDateStr != null && previousPaymentDateStr.isNotEmpty) {
        lastPaymentDate = DateTime.tryParse(previousPaymentDateStr);
      }

      // 결제일 계산 로직 추가 - 미래 날짜가 아니면 조정
      DateTime adjustedNextBillingDate = _calculateNextBillingDate(
        nextBillingDate ?? DateTime.now().add(const Duration(days: 30)),
        billingCycle,
      );
      // 주말/공휴일 보정
      adjustedNextBillingDate =
          BusinessDayUtil.nextBusinessDay(adjustedNextBillingDate);

      final model = SubscriptionModel(
        id: DateTime.now().millisecondsSinceEpoch.toString(),
        serviceName: serviceName,
        monthlyCost: monthlyCost,
        billingCycle: billingCycle,
        nextBillingDate: adjustedNextBillingDate,
        isAutoDetected: true,
        repeatCount: actualRepeatCount,
        lastPaymentDate: lastPaymentDate,
        websiteUrl: _extractWebsiteUrl(serviceName),
        currency: currency, // 통화 단위 설정
      );

      final suggestion = _extractPaymentCardSuggestion(message);
      return SmsScanResult(
        model: model,
        cardSuggestion: suggestion,
        rawMessage: message,
      );
    } catch (e) {
      return null;
    }
  }

  PaymentCardSuggestion? _extractPaymentCardSuggestion(String message) {
    if (message.isEmpty) return null;
    final issuer = _detectCardIssuer(message);
    final last4 = _detectCardLast4(message);
    if (issuer == null && last4 == null) {
      return null;
    }
    final loc = _loc();
    return PaymentCardSuggestion(
      issuerName: issuer ?? loc?.paymentCard ?? 'Payment card',
      last4: last4,
      source: 'sms',
    );
  }

  String? _detectCardIssuer(String message) {
    final normalized = message.toLowerCase();
    const issuerKeywords = {
      'KB국민카드': ['kb국민', '국민카드', 'kb card', 'kookmin'],
      '신한카드': ['신한', 'shinhan'],
      '우리카드': ['우리카드', 'woori'],
      '하나카드': ['하나카드', 'hana card', 'hana'],
      '농협카드': ['농협', 'nh', '농협카드'],
      'BC카드': ['bc카드', 'bc card'],
      '삼성카드': ['삼성카드', 'samsung card'],
      '롯데카드': ['롯데카드', 'lotte card'],
      '현대카드': ['현대카드', 'hyundai card'],
      '씨티카드': ['씨티카드', 'citi card', 'citibank'],
      '카카오뱅크': ['카카오뱅크', 'kakaobank'],
      '토스뱅크': ['토스뱅크', 'toss bank'],
      'Visa': ['visa'],
      'Mastercard': ['mastercard', 'master card'],
      'American Express': ['amex', 'american express'],
    };

    for (final entry in issuerKeywords.entries) {
      final match = entry.value.any((keyword) => normalized.contains(keyword));
      if (match) {
        return entry.key;
      }
    }

    return null;
  }

  String? _detectCardLast4(String message) {
    final patterns = [
      RegExp(r'\*{3,}\s*(\d{4})'),
      RegExp(r'끝번호\s*(\d{4})'),
      RegExp(r'마지막\s*(\d{4})'),
      RegExp(r'\((\d{4})\)'),
      RegExp(r'ending(?: in)?\s*(\d{4})', caseSensitive: false),
    ];

    for (final pattern in patterns) {
      final match = pattern.firstMatch(message);
      if (match != null && match.groupCount >= 1) {
        final candidate = match.group(1);
        if (candidate != null && candidate.length == 4) {
          return candidate;
        }
      }
    }

    return null;
  }

  // 다음 결제일 계산 (현재 날짜 기준으로 조정)
  DateTime _calculateNextBillingDate(
      DateTime billingDate, String billingCycle) {
    final now = DateTime.now();

    // 결제일이 이미 미래인 경우 그대로 반환
    if (billingDate.isAfter(now)) {
      return billingDate;
    }

    // 결제 주기별 다음 결제일 계산
    if (billingCycle == 'monthly') {
      int month = now.month;
      int year = now.year;

      // 현재 달의 결제일이 이미 지났으면 다음 달로 이동
      if (now.day >= billingDate.day) {
        month = month + 1;
        if (month > 12) {
          month = 1;
          year = year + 1;
        }
      }

      final dim = BusinessDayUtil.daysInMonth(year, month);
      final day = billingDate.day.clamp(1, dim);
      return DateTime(year, month, day);
    } else if (billingCycle == 'yearly') {
      // 올해의 결제일이 지났는지 확인
      final thisYearBilling =
          DateTime(now.year, billingDate.month, billingDate.day);
      if (thisYearBilling.isBefore(now)) {
        return DateTime(now.year + 1, billingDate.month, billingDate.day);
      } else {
        return thisYearBilling;
      }
    } else if (billingCycle == 'weekly') {
      // 가장 가까운 다음 주 같은 요일 계산
      final dayDifference = billingDate.weekday - now.weekday;
      final daysToAdd = dayDifference > 0 ? dayDifference : 7 + dayDifference;
      return now.add(Duration(days: daysToAdd));
    }

    // 기본 처리: 30일 후
    return now.add(const Duration(days: 30));
  }

  String? _extractWebsiteUrl(String serviceName) {
    // SubscriptionUrlMatcher 서비스를 사용하여 URL 매칭 시도
    final suggestedUrl = SubscriptionUrlMatcher.findMatchingUrl(serviceName);

    // 매칭된 URL이 있으면 반환, 없으면 기존 매핑 사용
    if (suggestedUrl != null && suggestedUrl.isNotEmpty) {
      return suggestedUrl;
    }

    // 기존 하드코딩된 매핑 (필요한 경우 폴백으로 사용)
    final Map<String, String> serviceUrls = {
      '넷플릭스': 'https://www.netflix.com',
      '디즈니플러스': 'https://www.disneyplus.com',
      '유튜브프리미엄': 'https://www.youtube.com',
      'YouTube Premium': 'https://www.youtube.com',
      '애플 iCloud': 'https://www.icloud.com',
      'Microsoft 365': 'https://www.microsoft.com/microsoft-365',
      '멜론': 'https://www.melon.com',
      '웨이브': 'https://www.wavve.com',
      'Apple Music': 'https://www.apple.com/apple-music',
      'Netflix': 'https://www.netflix.com',
      'Disney+': 'https://www.disneyplus.com',
      'Spotify': 'https://www.spotify.com',
    };

    return serviceUrls[serviceName];
  }

  // 메시지에서 통화 단위를 감지하는 함수
  String _detectCurrency(String message) {
    final dollarKeywords = [
      '\$', 'USD', 'dollar', '달러', 'dollars', 'US\$',
      // 해외 서비스 이름
      'Netflix US', 'Spotify Premium', 'Apple US', 'Amazon US', 'GitHub'
    ];

    // 특정 서비스명으로 통화 단위 결정
    final Map<String, String> serviceCurrencyMap = {
      'Netflix US': 'USD',
      'Spotify Premium': 'USD',
      'Spotify': 'USD',
      'GitHub': 'USD',
      'GitHub Pro': 'USD',
    };

    // 서비스명 기반 통화 단위 확인
    for (final service in serviceCurrencyMap.keys) {
      if (message.contains(service)) {
        Log.d('_detectCurrency: $service는 USD 서비스로 판별됨');
        return 'USD';
      }
    }

    // 메시지에 달러 관련 키워드가 있는지 확인
    for (final keyword in dollarKeywords) {
      if (message.toLowerCase().contains(keyword.toLowerCase())) {
        Log.d('_detectCurrency: USD 키워드 발견: $keyword');
        return 'USD';
      }
    }

    // 기본값은 원화
    return 'KRW';
  }

  AppLocalizations? _loc() {
    final ctx = navigatorKey.currentContext;
    if (ctx == null) return null;
    return AppLocalizations.of(ctx);
  }
}

const List<String> _paymentLikeKeywords = [
  '승인',
  '결제',
  '청구',
  'charged',
  'charge',
  'payment',
  'billed',
  'purchase',
];

const List<String> _blockedKeywords = [
  'otp',
  '인증',
  '보안',
  'verification',
  'code',
  '코드',
  'password',
  'pw',
  '일회성',
  '1회용',
  '보안문자',
];

bool _containsPaymentKeyword(String message) {
  if (message.isEmpty) return false;
  final normalized = message.toLowerCase();
  return _paymentLikeKeywords.any(
    (keyword) => normalized.contains(keyword.toLowerCase()),
  );
}

bool _containsBlockedKeyword(String message) {
  if (message.isEmpty) return false;
  final normalized = message.toLowerCase();
  return _blockedKeywords.any(
    (keyword) => normalized.contains(keyword.toLowerCase()),
  );
}

bool _isEligibleSubscriptionSms(Map<String, dynamic> sms) {
  final amount = (sms['monthlyCost'] as num?)?.toDouble();
  if (amount == null || amount <= 0) {
    return false;
  }

  final message = sms['message'] as String? ?? '';
  final isPaymentLike =
      (sms['isPaymentLike'] as bool?) ?? _containsPaymentKeyword(message);
  final isBlocked =
      (sms['isBlocked'] as bool?) ?? _containsBlockedKeyword(message);

  if (!isPaymentLike || isBlocked) {
    return false;
  }

  return true;
}

// ===== Isolate 오프로딩용 Top-level 파서 =====

// 대량 SMS 파싱: plugin 객체를 직렬화한 맵 리스트를 받아 구독 후보 맵 리스트로 변환
List<Map<String, dynamic>> _parseRawSmsBatch(
    List<Map<String, dynamic>> messages) {
  final amountPatterns = <RegExp>[
    RegExp(r'(\d{1,3}(?:,\d{3})*(?:\.\d{1,2})?)\s*(?:원|₩)'),
    RegExp(r'(?:원|₩)\s*(\d{1,3}(?:,\d{3})*(?:\.\d{1,2})?)'),
    RegExp(r'(?:(?:US)?\$)\s*(\d+(?:\.\d{1,2})?)', caseSensitive: false),
    RegExp(r'(\d+(?:,\d{3})*(?:\.\d{1,2})?)\s*(?:USD|KRW)',
        caseSensitive: false),
    RegExp(r'(?:USD|KRW)\s*(\d+(?:,\d{3})*(?:\.\d{1,2})?)',
        caseSensitive: false),
    RegExp(r'(?:결제|승인)[^0-9]{0,12}(\d{1,3}(?:,\d{3})*(?:\.\d{1,2})?)'),
  ];

  final results = <Map<String, dynamic>>[];

  for (final m in messages) {
    final body = (m['body'] as String?) ?? '';
    final sender = (m['address'] as String?) ?? '';
    final dateMillis =
        (m['dateMillis'] as int?) ?? DateTime.now().millisecondsSinceEpoch;
    final date = DateTime.fromMillisecondsSinceEpoch(dateMillis);
    final serviceName = _isoExtractServiceName(body, sender);
    final amount = _isoExtractAmount(body, amountPatterns);
    final isPaymentLike = _containsPaymentKeyword(body);
    final isBlocked = _containsBlockedKeyword(body);
    final billingCycle = _isoExtractBillingCycle(body);
    final nextBillingDate =
        _isoCalculateNextBillingFromDate(date, billingCycle);
    final normalizedBody = _isoNormalizeBody(body);

    results.add({
      'serviceName': serviceName,
      'address': sender,
      'monthlyCost': amount,
      'billingCycle': billingCycle,
      'message': body,
      'normalizedBody': normalizedBody,
      'nextBillingDate': nextBillingDate.toIso8601String(),
      'previousPaymentDate': date.toIso8601String(),
      'isPaymentLike': isPaymentLike,
      'isBlocked': isBlocked,
    });
  }

  return results;
}

String _isoExtractServiceName(String body, String sender) {
  final servicePatterns = {
    'netflix': '넷플릭스',
    'youtube': '유튜브 프리미엄',
    'spotify': 'Spotify',
    'disney': '디즈니플러스',
    'apple': 'Apple',
    'microsoft': 'Microsoft',
    'github': 'GitHub',
    'adobe': 'Adobe',
    '멜론': '멜론',
    '웨이브': '웨이브',
  };

  final combined = '$body $sender'.toLowerCase();
  for (final e in servicePatterns.entries) {
    if (combined.contains(e.key)) return e.value;
  }
  return _isoExtractServiceNameFromSender(sender);
}

String _isoExtractServiceNameFromSender(String sender) {
  if (RegExp(r'^\d+$').hasMatch(sender)) {
    // Isolate에서 실행되므로 하드코딩 사용 (Flutter 바인딩 접근 불가)
    return 'Unknown service';
  }
  return sender.replaceAll(RegExp(r'[^\w\s가-힣]'), '').trim();
}

double? _isoExtractAmount(String body, List<RegExp> patterns) {
  for (final pattern in patterns) {
    final match = pattern.firstMatch(body);
    if (match != null) {
      var amountStr = match.group(1) ?? '';
      amountStr = amountStr.replaceAll(',', '');
      final parsed = double.tryParse(amountStr);
      if (parsed != null) return parsed;
    }
  }
  return null;
}

String _isoExtractBillingCycle(String body) {
  if (body.contains('월') ||
      body.toLowerCase().contains('monthly') ||
      body.contains('매월')) {
    return 'monthly';
  } else if (body.contains('년') ||
      body.toLowerCase().contains('yearly') ||
      body.toLowerCase().contains('annual')) {
    return 'yearly';
  } else if (body.contains('주') || body.toLowerCase().contains('weekly')) {
    return 'weekly';
  }
  return 'monthly';
}

String _isoNormalizeBody(String body) {
  final patterns = <RegExp>[
    RegExp(r'\d{4}[./-]\d{1,2}[./-]\d{1,2}'),
    RegExp(r'\d{1,2}[./-]\d{1,2}[./-]\d{2,4}'),
    RegExp(r'\d{4}\s*년\s*\d{1,2}\s*월\s*\d{1,2}\s*일'),
    RegExp(r'\d{1,2}\s*월\s*\d{1,2}\s*일'),
    RegExp(r'\d{1,2}:\d{2}'),
  ];

  var normalized = body;
  for (final pattern in patterns) {
    normalized = normalized.replaceAll(pattern, ' ');
  }

  return normalized.replaceAll(RegExp(r'\s+'), ' ').trim().toLowerCase();
}

DateTime _isoCalculateNextBillingFromDate(
    DateTime lastDate, String billingCycle) {
  switch (billingCycle) {
    case 'monthly':
      return DateTime(lastDate.year, lastDate.month + 1, lastDate.day);
    case 'yearly':
      return DateTime(lastDate.year + 1, lastDate.month, lastDate.day);
    case 'weekly':
      return lastDate.add(const Duration(days: 7));
    default:
      return lastDate.add(const Duration(days: 30));
  }
}

Map<String, List<Map<String, dynamic>>> _groupMessagesByIdentifier(
    List<dynamic> smsList) {
  final Map<String, List<Map<String, dynamic>>> groups = {};

  for (final smsEntry in smsList) {
    if (smsEntry is! Map) continue;
    final sms = Map<String, dynamic>.from(smsEntry as Map<String, dynamic>);
    final serviceName = (sms['serviceName'] as String?)?.trim();
    final address = (sms['address'] as String?)?.trim();
    final sender = (sms['sender'] as String?)?.trim();

    final unknownLabel = _unknownServiceLabel();
    String key = (serviceName != null &&
            serviceName.isNotEmpty &&
            serviceName != unknownLabel)
        ? serviceName
        : (address?.isNotEmpty == true
            ? address!
            : (sender?.isNotEmpty == true ? sender! : unknownLabel));

    groups.putIfAbsent(key, () => []).add(sms);
  }

  return groups;
}

class _RepeatDetectionResult {
  _RepeatDetectionResult({
    required this.baseMessage,
    required this.repeatCount,
  });

  final Map<String, dynamic> baseMessage;
  final int repeatCount;
}

enum _MatchType { none, monthly, yearly, identical }

String _unknownServiceLabel() {
  final ctx = navigatorKey.currentContext;
  if (ctx == null) return 'Unknown service';
  return AppLocalizations.of(ctx).unknownService;
}

class _MatchedPair {
  _MatchedPair(this.first, this.second, this.type);

  final int first;
  final int second;
  final _MatchType type;
}

_RepeatDetectionResult? _detectRepeatingSubscriptions(
    List<Map<String, dynamic>> messages) {
  if (messages.length < 2) return null;

  final sorted = messages.map((sms) => Map<String, dynamic>.from(sms)).toList()
    ..sort((a, b) {
      final da = _parsePaymentDate(a['previousPaymentDate']);
      final db = _parsePaymentDate(b['previousPaymentDate']);
      return (db ?? DateTime.fromMillisecondsSinceEpoch(0))
          .compareTo(da ?? DateTime.fromMillisecondsSinceEpoch(0));
    });

  final matchedIndices = <int>{};
  final matchedPairs = <_MatchedPair>[];

  for (int i = 0; i < sorted.length - 1; i++) {
    for (int j = i + 1; j < sorted.length && j <= i + 5; j++) {
      final matchType = _evaluateMatch(sorted[i], sorted[j]);
      if (matchType == _MatchType.none) continue;
      matchedIndices.add(i);
      matchedIndices.add(j);
      matchedPairs.add(_MatchedPair(i, j, matchType));
      break;
    }
  }

  if (matchedIndices.length < 2) return null;

  final hasValidInterval = matchedPairs.any((pair) =>
      pair.type == _MatchType.monthly || pair.type == _MatchType.yearly);
  if (!hasValidInterval) return null;

  final baseIndex = matchedIndices
      .reduce((value, element) => value < element ? value : element);
  final baseMessage = Map<String, dynamic>.from(sorted[baseIndex]);

  final overrideDate = _deriveNextBillingDate(sorted, matchedPairs);
  if (overrideDate != null) {
    baseMessage['overrideNextBillingDate'] = overrideDate.toIso8601String();
  }

  return _RepeatDetectionResult(
    baseMessage: baseMessage,
    repeatCount: matchedIndices.length,
  );
}

_MatchType _evaluateMatch(
    Map<String, dynamic> recent, Map<String, dynamic> previous) {
  final amountMatch = _matchByAmountAndInterval(recent, previous);
  if (amountMatch != _MatchType.none) {
    return amountMatch;
  }

  if (_areBodiesEquivalent(recent, previous)) {
    final inferredInterval = _classifyIntervalByDates(recent, previous);
    return inferredInterval == _MatchType.none
        ? _MatchType.identical
        : inferredInterval;
  }

  return _MatchType.none;
}

_MatchType _matchByAmountAndInterval(
    Map<String, dynamic> a, Map<String, dynamic> b) {
  final amountA = (a['monthlyCost'] as num?)?.toDouble();
  final amountB = (b['monthlyCost'] as num?)?.toDouble();
  if (amountA == null || amountB == null) return _MatchType.none;
  if (!_isAmountSimilar(amountA, amountB)) return _MatchType.none;
  return _classifyIntervalByDates(a, b);
}

_MatchType _classifyIntervalByDates(
    Map<String, dynamic> a, Map<String, dynamic> b) {
  final dateA = _parsePaymentDate(a['previousPaymentDate']);
  final dateB = _parsePaymentDate(b['previousPaymentDate']);
  if (dateA == null || dateB == null) return _MatchType.none;
  final diffDays = (dateA.difference(dateB).inDays).abs();
  if (diffDays >= 27 && diffDays <= 34) {
    return _MatchType.monthly;
  }
  if (diffDays >= 350 && diffDays <= 380) {
    return _MatchType.yearly;
  }
  return _MatchType.none;
}

bool _areBodiesEquivalent(Map<String, dynamic> a, Map<String, dynamic> b) {
  final normalizedA = _getNormalizedBody(a);
  final normalizedB = _getNormalizedBody(b);
  if (normalizedA.isEmpty || normalizedB.isEmpty) return false;
  return normalizedA == normalizedB;
}

String _getNormalizedBody(Map<String, dynamic> sms) {
  final cached = sms['normalizedBody'] as String?;
  if (cached != null && cached.isNotEmpty) return cached;
  final message = sms['message'] as String? ?? '';
  final normalized = _isoNormalizeBody(message);
  sms['normalizedBody'] = normalized;
  return normalized;
}

DateTime? _deriveNextBillingDate(
    List<Map<String, dynamic>> sorted, List<_MatchedPair> pairs) {
  if (pairs.isEmpty) return null;

  final targetPair = pairs.firstWhere(
    (pair) => pair.type == _MatchType.monthly || pair.type == _MatchType.yearly,
    orElse: () => pairs.first,
  );

  final recent = sorted[targetPair.first];
  final previous = sorted[targetPair.second];
  final recentDate = _parsePaymentDate(recent['previousPaymentDate']);
  final prevDate = _parsePaymentDate(previous['previousPaymentDate']);

  return _calculateNextBillingFromPair(recentDate, prevDate, targetPair.type);
}

DateTime? _calculateNextBillingFromPair(
    DateTime? recentDate, DateTime? prevDate, _MatchType type) {
  if (recentDate == null) return null;

  if (type == _MatchType.monthly) {
    DateTime candidate = _addMonths(recentDate, 1);
    while (!candidate.isAfter(DateTime.now())) {
      candidate = _addMonths(candidate, 1);
    }
    return BusinessDayUtil.nextBusinessDay(candidate);
  }

  if (type == _MatchType.yearly) {
    DateTime candidate = DateTime(
      recentDate.year + 1,
      recentDate.month,
      _clampDay(
        recentDate.day,
        BusinessDayUtil.daysInMonth(recentDate.year + 1, recentDate.month),
      ),
    );
    while (!candidate.isAfter(DateTime.now())) {
      candidate = DateTime(candidate.year + 1, candidate.month, candidate.day);
    }
    return BusinessDayUtil.nextBusinessDay(candidate);
  }

  return _inferMonthlyNextBilling(recentDate, prevDate);
}

DateTime? _inferMonthlyNextBilling(DateTime recentDate, DateTime? prevDate) {
  int baseDay = recentDate.day;

  if (prevDate != null) {
    final candidate = DateTime(prevDate.year, prevDate.month, baseDay);
    if (BusinessDayUtil.isWeekend(candidate)) {
      final diff = prevDate.difference(candidate).inDays;
      if (diff < 1 || diff > 3) {
        baseDay = prevDate.day;
      }
    }
  }

  final now = DateTime.now();
  int year = now.year;
  int month = now.month;
  if (now.day >= baseDay) {
    month += 1;
    if (month > 12) {
      month = 1;
      year += 1;
    }
  }

  final dim = BusinessDayUtil.daysInMonth(year, month);
  final day = _clampDay(baseDay, dim);
  var nextBilling = DateTime(year, month, day);
  return BusinessDayUtil.nextBusinessDay(nextBilling);
}

DateTime _addMonths(DateTime date, int months) {
  final totalMonths = (date.month - 1) + months;
  final year = date.year + totalMonths ~/ 12;
  final month = totalMonths % 12 + 1;
  final dim = BusinessDayUtil.daysInMonth(year, month);
  final day = _clampDay(date.day, dim);
  return DateTime(year, month, day);
}

int _clampDay(int day, int maxDay) {
  if (day < 1) return 1;
  if (day > maxDay) return maxDay;
  return day;
}

DateTime? _parsePaymentDate(dynamic value) {
  if (value is DateTime) return value;
  if (value is String && value.isNotEmpty) {
    return DateTime.tryParse(value);
  }
  return null;
}

bool _isAmountSimilar(double a, double b) {
  final diff = (a - b).abs();
  final base = math.max(a.abs(), b.abs());
  final tolerance = base * 0.01; // 1% 허용
  final minTolerance = base < 10 ? 0.1 : 1.0;
  return diff <= math.max(tolerance, minTolerance);
}
