import 'dart:convert';
import 'dart:math';

import 'package:hive_flutter/hive_flutter.dart';
import 'package:http/http.dart' as http;
import 'package:lunchpick/core/constants/api_keys.dart';
import 'package:lunchpick/core/utils/app_logger.dart';
import 'package:lunchpick/domain/entities/weather_info.dart';
import 'package:lunchpick/domain/repositories/weather_repository.dart';

class WeatherRepositoryImpl implements WeatherRepository {
  static const String _boxName = 'weather_cache';
  static const String _keyCachedWeather = 'cached_weather';
  static const String _keyLastUpdateTime = 'last_update_time';
  static const Duration _cacheValidDuration = Duration(hours: 1);

  Future<Box> get _box async => await Hive.openBox(_boxName);

  @override
  Future<WeatherInfo> getCurrentWeather({
    required double latitude,
    required double longitude,
  }) async {
    final cached = await getCachedWeather();

    try {
      final weather = await _fetchWeatherFromKma(
        latitude: latitude,
        longitude: longitude,
      );
      await cacheWeatherInfo(weather);
      return weather;
    } catch (_) {
      if (cached != null) {
        return cached;
      }
      return WeatherInfo(
        current: WeatherData(
          temperature: 20,
          isRainy: false,
          description: '날씨 정보를 불러오지 못했어요',
        ),
        nextHour: WeatherData(
          temperature: 20,
          isRainy: false,
          description: '날씨 정보를 불러오지 못했어요',
        ),
      );
    }
  }

  @override
  Future<WeatherInfo?> getCachedWeather() async {
    final box = await _box;

    // 캐시가 유효한지 확인
    final isValid = await _isCacheValid();
    if (!isValid) {
      return null;
    }

    // 캐시된 데이터 가져오기
    final cachedData = box.get(_keyCachedWeather);
    if (cachedData == null) {
      return null;
    }

    try {
      // 안전한 타입 변환
      if (cachedData is! Map) {
        AppLogger.debug(
          'WeatherCache: Invalid data type - expected Map but got ${cachedData.runtimeType}',
        );
        await clearWeatherCache();
        return null;
      }

      final Map<String, dynamic> weatherMap = Map<String, dynamic>.from(
        cachedData,
      );

      // Map 구조 검증
      if (!weatherMap.containsKey('current') ||
          !weatherMap.containsKey('nextHour')) {
        AppLogger.debug(
          'WeatherCache: Missing required fields in weather data',
        );
        await clearWeatherCache();
        return null;
      }

      return _weatherInfoFromMap(weatherMap);
    } catch (e) {
      // 캐시 데이터가 손상된 경우
      AppLogger.error(
        'WeatherCache: Error parsing cached weather data',
        error: e,
      );
      await clearWeatherCache();
      return null;
    }
  }

  @override
  Future<void> cacheWeatherInfo(WeatherInfo weatherInfo) async {
    final box = await _box;

    // WeatherInfo를 Map으로 변환하여 저장
    final weatherMap = _weatherInfoToMap(weatherInfo);
    await box.put(_keyCachedWeather, weatherMap);
    await box.put(_keyLastUpdateTime, DateTime.now().toIso8601String());
  }

  @override
  Future<void> clearWeatherCache() async {
    final box = await _box;
    await box.delete(_keyCachedWeather);
    await box.delete(_keyLastUpdateTime);
  }

  @override
  Future<bool> isWeatherUpdateNeeded() async {
    final box = await _box;

    // 캐시된 날씨 정보가 없으면 업데이트 필요
    if (!box.containsKey(_keyCachedWeather)) {
      return true;
    }

    // 캐시가 유효한지 확인
    return !(await _isCacheValid());
  }

  Future<bool> _isCacheValid() async {
    final box = await _box;

    final lastUpdateTimeStr = box.get(_keyLastUpdateTime);
    if (lastUpdateTimeStr == null) {
      return false;
    }

    try {
      // 날짜 파싱 시도
      final lastUpdateTime = DateTime.tryParse(lastUpdateTimeStr);
      if (lastUpdateTime == null) {
        AppLogger.debug(
          'WeatherCache: Invalid date format in cache: $lastUpdateTimeStr',
        );
        return false;
      }

      final now = DateTime.now();
      final difference = now.difference(lastUpdateTime);

      return difference < _cacheValidDuration;
    } catch (e) {
      AppLogger.error('WeatherCache: Error checking cache validity', error: e);
      return false;
    }
  }

  Map<String, dynamic> _weatherInfoToMap(WeatherInfo weatherInfo) {
    return {
      'current': {
        'temperature': weatherInfo.current.temperature,
        'isRainy': weatherInfo.current.isRainy,
        'description': weatherInfo.current.description,
      },
      'nextHour': {
        'temperature': weatherInfo.nextHour.temperature,
        'isRainy': weatherInfo.nextHour.isRainy,
        'description': weatherInfo.nextHour.description,
      },
    };
  }

  WeatherInfo _weatherInfoFromMap(Map<String, dynamic> map) {
    try {
      // current 필드 검증
      final currentRaw = map['current'];
      final currentMap = currentRaw is Map
          ? Map<String, dynamic>.from(currentRaw)
          : null;
      if (currentMap == null) {
        throw FormatException('Missing current weather data');
      }

      // nextHour 필드 검증
      final nextHourRaw = map['nextHour'];
      final nextHourMap = nextHourRaw is Map
          ? Map<String, dynamic>.from(nextHourRaw)
          : null;
      if (nextHourMap == null) {
        throw FormatException('Missing nextHour weather data');
      }

      // 필수 필드 검증 및 기본값 제공
      final currentTemp = currentMap['temperature'] as num? ?? 20;
      final currentRainy = currentMap['isRainy'] as bool? ?? false;
      final currentDesc = currentMap['description'] as String? ?? '알 수 없음';

      final nextTemp = nextHourMap['temperature'] as num? ?? 20;
      final nextRainy = nextHourMap['isRainy'] as bool? ?? false;
      final nextDesc = nextHourMap['description'] as String? ?? '알 수 없음';

      return WeatherInfo(
        current: WeatherData(
          temperature: currentTemp.round(),
          isRainy: currentRainy,
          description: currentDesc,
        ),
        nextHour: WeatherData(
          temperature: nextTemp.round(),
          isRainy: nextRainy,
          description: nextDesc,
        ),
      );
    } catch (e) {
      AppLogger.error(
        'WeatherCache: Error converting map to WeatherInfo',
        error: e,
        stackTrace: StackTrace.current,
      );
      AppLogger.debug('WeatherCache: Map data: $map');
      rethrow;
    }
  }

  Future<WeatherInfo> _fetchWeatherFromKma({
    required double latitude,
    required double longitude,
  }) async {
    final serviceKey = _encodeServiceKey(ApiKeys.weatherServiceKey);
    if (serviceKey.isEmpty) {
      throw Exception('기상청 서비스 키가 설정되지 않았습니다.');
    }

    final gridPoint = _latLonToGrid(latitude, longitude);
    final baseDateTime = _resolveBaseDateTime();

    final ncstUri = Uri.https(
      'apis.data.go.kr',
      '/1360000/VilageFcstInfoService_2.0/getUltraSrtNcst',
      {
        'serviceKey': serviceKey,
        'numOfRows': '100',
        'pageNo': '1',
        'dataType': 'JSON',
        'base_date': baseDateTime.date,
        'base_time': baseDateTime.ncstTime,
        'nx': gridPoint.x.toString(),
        'ny': gridPoint.y.toString(),
      },
    );

    final fcstUri = Uri.https(
      'apis.data.go.kr',
      '/1360000/VilageFcstInfoService_2.0/getUltraSrtFcst',
      {
        'serviceKey': serviceKey,
        'numOfRows': '200',
        'pageNo': '1',
        'dataType': 'JSON',
        'base_date': baseDateTime.date,
        'base_time': baseDateTime.fcstTime,
        'nx': gridPoint.x.toString(),
        'ny': gridPoint.y.toString(),
      },
    );

    final ncstItems = await _requestKmaItems(ncstUri);
    final fcstItems = await _requestKmaItems(fcstUri);

    final currentTemp = _extractLatestValue(ncstItems, 'T1H')?.round();
    final currentPty = _extractLatestValue(ncstItems, 'PTY')?.round() ?? 0;
    final currentSky = _extractLatestValue(ncstItems, 'SKY')?.round() ?? 1;

    final now = DateTime.now();
    final nextHourData = _extractForecast(fcstItems, after: now);
    final nextTemp = nextHourData.temperature?.round();
    final nextPty = nextHourData.pty ?? 0;
    final nextSky = nextHourData.sky ?? 1;

    final currentWeather = WeatherData(
      temperature: currentTemp ?? 20,
      isRainy: _isRainy(currentPty),
      description: _describeWeather(currentSky, currentPty),
    );

    final nextWeather = WeatherData(
      temperature: nextTemp ?? currentTemp ?? 20,
      isRainy: _isRainy(nextPty),
      description: _describeWeather(nextSky, nextPty),
    );

    return WeatherInfo(current: currentWeather, nextHour: nextWeather);
  }

  Future<List<dynamic>> _requestKmaItems(Uri uri) async {
    final response = await http.get(uri);
    if (response.statusCode != 200) {
      throw Exception('Weather API 호출 실패: ${response.statusCode}');
    }

    final body = jsonDecode(response.body) as Map<String, dynamic>;
    final items = body['response']?['body']?['items']?['item'];
    if (items is List<dynamic>) {
      return items;
    }
    throw Exception('Weather API 응답 파싱 실패');
  }

  double? _extractLatestValue(List<dynamic> items, String category) {
    final filtered = items.where((item) => item['category'] == category);
    if (filtered.isEmpty) return null;
    final sorted = filtered.toList()
      ..sort((a, b) {
        final dateA = a['baseDate'] as String? ?? '';
        final timeA = a['baseTime'] as String? ?? '';
        final dateB = b['baseDate'] as String? ?? '';
        final timeB = b['baseTime'] as String? ?? '';
        final dtA = _parseKmaDateTime(dateA, timeA);
        final dtB = _parseKmaDateTime(dateB, timeB);
        return dtB.compareTo(dtA);
      });
    final value = sorted.first['obsrValue'] ?? sorted.first['fcstValue'];
    if (value is num) return value.toDouble();
    if (value is String) return double.tryParse(value);
    return null;
  }

  ({double? temperature, int? pty, int? sky}) _extractForecast(
    List<dynamic> items, {
    required DateTime after,
  }) {
    DateTime? targetTime;
    double? temperature;
    int? pty;
    int? sky;

    DateTime fcstDateTime(Map<String, dynamic> item) {
      final date = item['fcstDate'] as String? ?? '';
      final time = item['fcstTime'] as String? ?? '';
      return _parseKmaDateTime(date, time);
    }

    for (final item in items) {
      final dt = fcstDateTime(item as Map<String, dynamic>);
      if (!dt.isAfter(after)) continue;
      if (targetTime == null || dt.isBefore(targetTime)) {
        targetTime = dt;
      }
    }

    if (targetTime == null) {
      return (temperature: null, pty: null, sky: null);
    }

    for (final item in items) {
      final map = item as Map<String, dynamic>;
      final dt = fcstDateTime(map);
      if (dt != targetTime) continue;

      final category = map['category'];
      final value = map['fcstValue'];
      if (value == null) continue;

      if (category == 'T1H' && temperature == null) {
        temperature = value is num
            ? value.toDouble()
            : double.tryParse('$value');
      } else if (category == 'PTY' && pty == null) {
        pty = value is num ? value.toInt() : int.tryParse('$value');
      } else if (category == 'SKY' && sky == null) {
        sky = value is num ? value.toInt() : int.tryParse('$value');
      }
    }

    return (temperature: temperature, pty: pty, sky: sky);
  }

  _GridPoint _latLonToGrid(double lat, double lon) {
    const re = 6371.00877;
    const grid = 5.0;
    const slat1 = 30.0 * pi / 180.0;
    const slat2 = 60.0 * pi / 180.0;
    const olon = 126.0 * pi / 180.0;
    const olat = 38.0 * pi / 180.0;
    const xo = 43.0;
    const yo = 136.0;

    final sn =
        log(cos(slat1) / cos(slat2)) /
        log(tan(pi * 0.25 + slat2 * 0.5) / tan(pi * 0.25 + slat1 * 0.5));
    final sf = pow(tan(pi * 0.25 + slat1 * 0.5), sn) * cos(slat1) / sn;
    final ro = re / grid * sf / pow(tan(pi * 0.25 + olat * 0.5), sn);
    final ra =
        re / grid * sf / pow(tan(pi * 0.25 + (lat * pi / 180.0) * 0.5), sn);
    var theta = lon * pi / 180.0 - olon;
    if (theta > pi) theta -= 2.0 * pi;
    if (theta < -pi) theta += 2.0 * pi;
    theta *= sn;

    final x = (ra * sin(theta) + xo + 0.5).floor();
    final y = (ro - ra * cos(theta) + yo + 0.5).floor();
    return _GridPoint(x: x, y: y);
  }

  bool _isRainy(int pty) => pty > 0;

  String _describeWeather(int sky, int pty) {
    if (pty == 1) return '비';
    if (pty == 2) return '비/눈';
    if (pty == 3) return '눈';
    if (pty == 4) return '소나기';
    if (pty == 5) return '빗방울';
    if (pty == 6) return '빗방울/눈날림';
    if (pty == 7) return '눈날림';

    switch (sky) {
      case 1:
        return '맑음';
      case 3:
        return '구름 많음';
      case 4:
        return '흐림';
      default:
        return '맑음';
    }
  }

  /// 서비스 키를 안전하게 URL 인코딩한다.
  /// 이미 인코딩된 값(%)이 포함되어 있으면 그대로 사용한다.
  String _encodeServiceKey(String key) {
    if (key.isEmpty) return '';
    if (key.contains('%')) return key;
    return Uri.encodeComponent(key);
  }

  _BaseDateTime _resolveBaseDateTime() {
    final now = DateTime.now();

    // 초단기실황은 매시 정시 발표(정시+10분 이후 호출 권장)
    // 초단기예보는 매시 30분 발표(30분+10분 이후 호출 권장)
    final ncstAnchor = now.minute >= 10
        ? DateTime(now.year, now.month, now.day, now.hour, 0)
        : DateTime(now.year, now.month, now.day, now.hour - 1, 0);
    final fcstAnchor = now.minute >= 40
        ? DateTime(now.year, now.month, now.day, now.hour, 30)
        : DateTime(now.year, now.month, now.day, now.hour - 1, 30);

    final date = _formatDate(fcstAnchor); // 둘 다 같은 날짜/시점 기준
    final ncstTime = _formatTime(ncstAnchor);
    final fcstTime = _formatTime(fcstAnchor);

    return _BaseDateTime(date: date, ncstTime: ncstTime, fcstTime: fcstTime);
  }

  String _formatDate(DateTime dt) {
    final y = dt.year.toString().padLeft(4, '0');
    final m = dt.month.toString().padLeft(2, '0');
    final d = dt.day.toString().padLeft(2, '0');
    return '$y$m$d';
  }

  String _formatTime(DateTime dt) {
    final h = dt.hour.toString().padLeft(2, '0');
    final m = dt.minute.toString().padLeft(2, '0');
    return '$h$m';
  }

  DateTime _parseKmaDateTime(String date, String time) {
    final year = int.parse(date.substring(0, 4));
    final month = int.parse(date.substring(4, 6));
    final day = int.parse(date.substring(6, 8));
    final hour = int.parse(time.substring(0, 2));
    final minute = int.parse(time.substring(2, 4));
    return DateTime(year, month, day, hour, minute);
  }
}

class _GridPoint {
  final int x;
  final int y;

  _GridPoint({required this.x, required this.y});
}

class _BaseDateTime {
  final String date;
  final String ncstTime;
  final String fcstTime;

  _BaseDateTime({
    required this.date,
    required this.ncstTime,
    required this.fcstTime,
  });
}
