import 'dart:math';

import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:html/parser.dart' as html_parser;
import 'package:lunchpick/core/utils/app_logger.dart';
import 'package:lunchpick/domain/entities/restaurant.dart';
import 'package:uuid/uuid.dart';

import '../../api/naver/naver_graphql_queries.dart';
import '../../api/naver/naver_local_search_api.dart';
import '../../api/naver_api_client.dart';
import '../../../core/errors/network_exceptions.dart';
import '../../../core/utils/category_mapper.dart';
import 'naver_html_parser.dart';

/// 네이버 지도 URL 파서
/// 네이버 지도 URL에서 식당 정보를 추출합니다.
/// NaverApiClient를 사용하여 네트워크 통신을 처리합니다.
class NaverMapParser {
  // URL 관련 상수
  static const String _naverMapBaseUrl = 'https://map.naver.com';

  // 정규식 패턴
  static final RegExp _placeIdRegex = RegExp(
    r'(?:/p/(?:restaurant|entry/place)/|/place/)(\d+)',
  );
  static final RegExp _pinIdRegex = RegExp(r'pinId["=](\d+)');
  static final RegExp _shortUrlRegex = RegExp(r'naver\.me/([a-zA-Z0-9]+)$');

  // 기본 좌표 (서울 시청)
  static const double _defaultLatitude = 37.5666805;
  static const double _defaultLongitude = 126.9784147;

  // API 요청 관련 상수
  static const int _shortDelayMillis = 500;
  static const int _longDelayMillis = 1000;
  static const int _searchDisplayCount = 10;
  static const double _coordinateConversionFactor = 10000000.0;

  final NaverApiClient _apiClient;
  final NaverHtmlParser _htmlParser = NaverHtmlParser();
  final Uuid _uuid = const Uuid();
  bool _isDisposed = false;

  NaverMapParser({NaverApiClient? apiClient})
    : _apiClient = apiClient ?? NaverApiClient();

  /// 네이버 지도 URL에서 식당 정보를 파싱합니다.
  ///
  /// 지원하는 URL 형식:
  /// - https://map.naver.com/p/restaurant/1234567890
  /// - https://naver.me/abcdefgh
  ///
  /// [userLatitude]와 [userLongitude]를 제공하면 중복 상호명이 있을 때
  /// 가장 가까운 위치의 식당을 선택합니다.
  Future<Restaurant> parseRestaurantFromUrl(
    String url, {
    double? userLatitude,
    double? userLongitude,
  }) async {
    if (_isDisposed) {
      throw NaverMapParseException('이미 dispose된 파서입니다');
    }
    try {
      AppLogger.debug('[naver_url] 원본 URL 수신: $url');

      // URL 유효성 검증
      if (!_isValidNaverUrl(url)) {
        throw NaverMapParseException('유효하지 않은 네이버 지도 URL입니다: $url');
      }

      // 짧은 URL인 경우 리다이렉트 처리
      final String finalUrl = await _apiClient.resolveShortUrl(url);

      AppLogger.debug('[naver_url] resolveShortUrl 결과: $finalUrl');

      // Place ID 추출 (10자리 숫자)
      final String? placeId = _extractPlaceId(finalUrl);
      if (placeId == null) {
        // 짧은 URL에서 직접 ID 추출 시도
        final shortUrlId = _extractShortUrlId(url);
        if (shortUrlId != null) {
          AppLogger.debug('[naver_url] 단축 URL ID를 Place ID로 사용: $shortUrlId');
          return _createFallbackRestaurant(shortUrlId, url);
        }
        throw NaverMapParseException('URL에서 Place ID를 추출할 수 없습니다: $url');
      }
      AppLogger.debug('[naver_url] Place ID 추출 성공: $placeId');

      // 단축 URL인 경우 특별 처리
      final isShortUrl = url.contains('naver.me');

      if (isShortUrl) {
        AppLogger.debug('NaverMapParser: 단축 URL 감지, 향상된 파싱 시작');

        try {
          // 한글 텍스트 추출 및 로컬 검색 API를 통한 정확한 정보 획득
          final restaurant = await _parseWithLocalSearch(
            placeId,
            finalUrl,
            userLatitude,
            userLongitude,
          );
          AppLogger.debug(
            '[naver_url] LocalSearch 파싱 성공: '
            'name=${restaurant.name}, road=${restaurant.roadAddress}',
          );
          return restaurant;
        } catch (e, stackTrace) {
          AppLogger.error(
            'NaverMapParser: 단축 URL 특별 처리 실패, 기본 파싱으로 전환 - $e',
            error: e,
            stackTrace: stackTrace,
          );
          // 실패 시 기본 파싱으로 계속 진행
        }
      }

      // GraphQL API로 식당 정보 가져오기 (기본 플로우)
      final restaurantData = await _fetchRestaurantFromGraphQL(
        placeId,
        userLatitude: userLatitude,
        userLongitude: userLongitude,
      );
      AppLogger.debug(
        '[naver_url] GraphQL/검색 파싱 결과 요약: '
        'name=${restaurantData['name']}, '
        'road=${restaurantData['roadAddress']}, '
        'phone=${restaurantData['phone']}',
      );
      return _createRestaurant(restaurantData, placeId, finalUrl);
    } catch (e) {
      if (e is NaverMapParseException) {
        rethrow;
      }
      if (e is RateLimitException) {
        rethrow;
      }
      if (e is NetworkException) {
        throw NaverMapParseException('네트워크 오류: ${e.message}');
      }
      throw NaverMapParseException('네이버 지도 파싱 중 오류가 발생했습니다: $e');
    }
  }

  /// URL이 유효한 네이버 지도 URL인지 확인
  bool _isValidNaverUrl(String url) {
    try {
      final Uri uri = Uri.parse(url);
      return uri.host.contains('naver.com') || uri.host.contains('naver.me');
    } catch (e) {
      return false;
    }
  }

  // _resolveFinalUrl 메서드는 이제 NaverApiClient.resolveShortUrl로 대체됨

  /// URL에서 Place ID 추출
  String? _extractPlaceId(String url) {
    final match = _placeIdRegex.firstMatch(url);
    if (match != null) return match.group(1);

    // 핀 공유 형식: pinId="1234567890" 또는 pinId=1234567890
    final pinMatch = _pinIdRegex.firstMatch(url);
    return pinMatch?.group(1);
  }

  /// 짧은 URL에서 ID 추출
  String? _extractShortUrlId(String url) {
    try {
      final match = _shortUrlRegex.firstMatch(url);
      return match?.group(1);
    } catch (e) {
      return null;
    }
  }

  /// GraphQL API로 식당 정보 가져오기
  Future<Map<String, dynamic>> _fetchRestaurantFromGraphQL(
    String placeId, {
    double? userLatitude,
    double? userLongitude,
  }) async {
    // 심플한 접근: URL로 직접 검색
    try {
      AppLogger.debug('NaverMapParser: URL 기반 검색 시작');

      // 네이버 지도 URL 구성
      final placeUrl = '$_naverMapBaseUrl/p/entry/place/$placeId';

      // Step 1: URL 자체로 검색 (가장 신뢰할 수 있는 방법)
      try {
        await Future.delayed(
          const Duration(milliseconds: _shortDelayMillis),
        ); // 429 방지

        final searchResults = await _apiClient.searchLocal(
          query: placeUrl,
          latitude: userLatitude,
          longitude: userLongitude,
          display: _searchDisplayCount,
        );
        AppLogger.debug(
          '[naver_url] URL 기반 검색 응답 개수: ${searchResults.length}, '
          '첫 번째: ${searchResults.isNotEmpty ? searchResults.first.title : '없음'}',
        );

        if (searchResults.isNotEmpty) {
          // place ID가 포함된 결과 찾기
          for (final result in searchResults) {
            if (result.link.contains(placeId)) {
              AppLogger.debug(
                'NaverMapParser: URL 검색으로 정확한 매칭 찾음 - ${result.title}',
              );
              return _convertSearchResultToData(result);
            }
          }

          // 정확한 매칭이 없으면 첫 번째 결과 사용
          AppLogger.debug(
            'NaverMapParser: URL 검색 첫 번째 결과 사용 - ${searchResults.first.title}',
          );
          return _convertSearchResultToData(searchResults.first);
        }
      } catch (e, stackTrace) {
        AppLogger.error(
          'NaverMapParser: URL 검색 실패 - $e',
          error: e,
          stackTrace: stackTrace,
        );
      }

      // Step 2: Place ID로 검색
      try {
        await Future.delayed(
          const Duration(milliseconds: _longDelayMillis),
        ); // 더 긴 지연

        final searchResults = await _apiClient.searchLocal(
          query: placeId,
          latitude: userLatitude,
          longitude: userLongitude,
          display: _searchDisplayCount,
        );
        AppLogger.debug(
          '[naver_url] Place ID 검색 응답 개수: ${searchResults.length}, '
          '첫 번째: ${searchResults.isNotEmpty ? searchResults.first.title : '없음'}',
        );

        if (searchResults.isNotEmpty) {
          AppLogger.debug(
            'NaverMapParser: Place ID 검색 결과 사용 - ${searchResults.first.title}',
          );
          return _convertSearchResultToData(searchResults.first);
        }
      } catch (e, stackTrace) {
        AppLogger.error(
          'NaverMapParser: Place ID 검색 실패 - $e',
          error: e,
          stackTrace: stackTrace,
        );

        // 429 에러인 경우 즉시 예외 발생
        if (e is DioException && e.response?.statusCode == 429) {
          throw RateLimitException(
            retryAfter: e.response?.headers['retry-after']?.firstOrNull,
            originalError: e,
          );
        }
      }
    } catch (e, stackTrace) {
      AppLogger.error(
        'NaverMapParser: URL 기반 검색 실패 - $e',
        error: e,
        stackTrace: stackTrace,
      );

      // 429 에러인 경우 즉시 예외 발생
      if (e is DioException && e.response?.statusCode == 429) {
        throw RateLimitException(
          retryAfter: e.response?.headers['retry-after']?.firstOrNull,
          originalError: e,
        );
      }
    }

    // 기존 GraphQL 방식으로 fallback (실패할 가능성 높지만 시도)
    // 첫 번째 시도: places 쿼리
    try {
      AppLogger.debug('NaverMapParser: Trying places query...');
      final response = await _apiClient.fetchGraphQL(
        operationName: 'getPlaceDetail',
        variables: {'id': placeId},
        query: NaverGraphQLQueries.placeDetailQuery,
      );
      AppLogger.debug(
        '[naver_url] places query 응답 keys: ${response.keys.toList()}',
      );

      // places 응답 처리 (배열일 수도 있음)
      final placesData = response['data']?['places'];
      if (placesData != null) {
        if (placesData is List && placesData.isNotEmpty) {
          return _extractPlaceData(placesData.first as Map<String, dynamic>);
        } else if (placesData is Map) {
          return _extractPlaceData(placesData as Map<String, dynamic>);
        }
      }
    } catch (e, stackTrace) {
      AppLogger.error(
        'NaverMapParser: places query failed - $e',
        error: e,
        stackTrace: stackTrace,
      );
    }

    // 두 번째 시도: nxPlaces 쿼리
    try {
      AppLogger.debug('NaverMapParser: Trying nxPlaces query...');
      final response = await _apiClient.fetchGraphQL(
        operationName: 'getPlaceDetail',
        variables: {'id': placeId},
        query: NaverGraphQLQueries.nxPlaceDetailQuery,
      );
      AppLogger.debug(
        '[naver_url] nxPlaces query 응답 keys: ${response.keys.toList()}',
      );

      // nxPlaces 응답 처리 (배열일 수도 있음)
      final nxPlacesData = response['data']?['nxPlaces'];
      if (nxPlacesData != null) {
        if (nxPlacesData is List && nxPlacesData.isNotEmpty) {
          return _extractPlaceData(nxPlacesData.first as Map<String, dynamic>);
        } else if (nxPlacesData is Map) {
          return _extractPlaceData(nxPlacesData as Map<String, dynamic>);
        }
      }
    } catch (e, stackTrace) {
      AppLogger.error(
        'NaverMapParser: nxPlaces query failed - $e',
        error: e,
        stackTrace: stackTrace,
      );
    }

    // 모든 GraphQL 시도 실패 시 HTML 파싱으로 fallback
    AppLogger.debug(
      'NaverMapParser: All GraphQL queries failed, falling back to HTML parsing',
    );
    return await _fallbackToHtmlParsing(placeId);
  }

  /// 검색 결과를 데이터 맵으로 변환
  Map<String, dynamic> _convertSearchResultToData(NaverLocalSearchResult item) {
    // 카테고리 파싱
    final categoryParts = item.category
        .split('>')
        .map((s) => s.trim())
        .toList();
    final category = categoryParts.isNotEmpty ? categoryParts.first : '음식점';
    final subCategory = categoryParts.length > 1
        ? categoryParts.last
        : category;

    return {
      'name': item.title,
      'category': category,
      'subCategory': subCategory,
      'address': item.address,
      'roadAddress': item.roadAddress,
      'phone': item.telephone,
      'description': item.description.isNotEmpty ? item.description : null,
      'latitude': item.mapy != null
          ? item.mapy! / _coordinateConversionFactor
          : _defaultLatitude,
      'longitude': item.mapx != null
          ? item.mapx! / _coordinateConversionFactor
          : _defaultLongitude,
      'businessHours': null, // Search API에서는 영업시간 정보 제공 안 함
    };
  }

  /// GraphQL 응답에서 데이터 추출
  Map<String, dynamic> _extractPlaceData(Map<String, dynamic> placeData) {
    // 카테고리 파싱
    final String? fullCategory = placeData['category'];
    String? category;
    String? subCategory;

    if (fullCategory != null) {
      final categoryParts = fullCategory
          .split('>')
          .map((s) => s.trim())
          .toList();
      category = categoryParts.isNotEmpty ? categoryParts.first : null;
      subCategory = categoryParts.length > 1 ? categoryParts.last : null;
    }

    return {
      'name': placeData['name'],
      'category': category,
      'subCategory': subCategory,
      'address': placeData['address'],
      'roadAddress': placeData['roadAddress'],
      'phone': placeData['phone'] ?? placeData['virtualPhone'],
      'description': placeData['description'],
      'latitude': placeData['location']?['lat'],
      'longitude': placeData['location']?['lng'],
      'businessHours': placeData['businessHours']?.isNotEmpty == true
          ? placeData['businessHours'][0]['description']
          : null,
    };
  }

  /// HTML 파싱으로 fallback
  Future<Map<String, dynamic>> _fallbackToHtmlParsing(String placeId) async {
    try {
      final finalUrl = '$_naverMapBaseUrl/p/entry/place/$placeId';
      final String html = await _apiClient.fetchMapPageHtml(finalUrl);
      final document = html_parser.parse(html);

      return _htmlParser.parseRestaurantInfo(document);
    } catch (e) {
      // 429 에러인 경우 RateLimitException으로 변환
      if (e.toString().contains('429')) {
        throw RateLimitException(originalError: e);
      }
      rethrow;
    }
  }

  /// Restaurant 객체 생성
  Restaurant _createRestaurant(
    Map<String, dynamic> data,
    String placeId,
    String url,
  ) {
    // 데이터 추출 및 기본값 처리
    final String name = data['name'] ?? '네이버 지도 장소 #$placeId';
    final String rawCategory = data['category'] ?? '음식점';
    final String? rawSubCategory = data['subCategory'];
    final String? description = data['description'];
    final String? phoneNumber = data['phone'];
    final String roadAddress = data['roadAddress'] ?? '';
    final String jibunAddress = data['address'] ?? '';
    final double? latitude = data['latitude'];
    final double? longitude = data['longitude'];
    final String? businessHours = data['businessHours'];

    // 카테고리 정규화
    final String normalizedCategory = CategoryMapper.normalizeNaverCategory(
      rawCategory,
      rawSubCategory,
    );
    final String finalSubCategory = rawSubCategory ?? rawCategory;

    // 좌표가 없는 경우 기본값 설정
    final double finalLatitude = latitude ?? _defaultLatitude;
    final double finalLongitude = longitude ?? _defaultLongitude;

    // 주소가 비어있는 경우 처리
    final String finalRoadAddress = roadAddress.isNotEmpty
        ? roadAddress
        : '주소 정보를 가져올 수 없습니다';
    final String finalJibunAddress = jibunAddress.isNotEmpty
        ? jibunAddress
        : '주소 정보를 가져올 수 없습니다';

    return Restaurant(
      id: _uuid.v4(),
      name: name,
      category: normalizedCategory,
      subCategory: finalSubCategory,
      description:
          description ?? '네이버 지도에서 가져온 장소입니다. 자세한 정보는 네이버 지도에서 확인해주세요.',
      phoneNumber: phoneNumber,
      roadAddress: finalRoadAddress,
      jibunAddress: finalJibunAddress,
      latitude: finalLatitude,
      longitude: finalLongitude,
      lastVisitDate: null,
      source: DataSource.NAVER,
      createdAt: DateTime.now(),
      updatedAt: DateTime.now(),
      naverPlaceId: placeId,
      naverUrl: url,
      businessHours: businessHours,
      lastVisited: null,
      visitCount: 0,
    );
  }

  /// 기본 정보로 Restaurant 생성 (Fallback)
  Restaurant _createFallbackRestaurant(String placeId, String url) {
    return Restaurant(
      id: _uuid.v4(),
      name: '네이버 지도 장소 #$placeId',
      category: '음식점',
      subCategory: '음식점',
      description: '네이버 지도에서 가져온 장소입니다. 자세한 정보는 네이버 지도에서 확인해주세요.',
      phoneNumber: null,
      roadAddress: '주소 정보를 가져올 수 없습니다',
      jibunAddress: '주소 정보를 가져올 수 없습니다',
      latitude: _defaultLatitude,
      longitude: _defaultLongitude,
      lastVisitDate: null,
      source: DataSource.NAVER,
      createdAt: DateTime.now(),
      updatedAt: DateTime.now(),
      naverPlaceId: placeId,
      naverUrl: url,
      businessHours: null,
      lastVisited: null,
      visitCount: 0,
    );
  }

  /// 단축 URL을 위한 향상된 파싱 메서드
  /// 한글 텍스트를 추출하고 로컬 검색 API를 통해 정확한 정보를 획득
  Future<Restaurant> _parseWithLocalSearch(
    String placeId,
    String finalUrl,
    double? userLatitude,
    double? userLongitude,
  ) async {
    if (kDebugMode) {
      AppLogger.debug('NaverMapParser: 단축 URL 향상된 파싱 시작');
    }

    // 1. 한글 텍스트 추출
    final koreanData = await _apiClient.fetchKoreanTextsFromPcmap(placeId);

    if (koreanData['success'] != true || koreanData['koreanTexts'] == null) {
      throw NaverMapParseException('한글 텍스트 추출 실패');
    }

    final koreanTexts = koreanData['koreanTexts'] as List<dynamic>;

    // 상호명 우선순위 결정
    String searchQuery = '';
    if (koreanData['jsonLdName'] != null) {
      searchQuery = koreanData['jsonLdName'] as String;
      if (kDebugMode) {
        AppLogger.debug('NaverMapParser: JSON-LD 상호명 사용 - $searchQuery');
      }
    } else if (koreanData['apolloStateName'] != null) {
      searchQuery = koreanData['apolloStateName'] as String;
      if (kDebugMode) {
        AppLogger.debug('NaverMapParser: Apollo State 상호명 사용 - $searchQuery');
      }
    } else if (koreanTexts.isNotEmpty) {
      searchQuery = koreanTexts.first as String;
      if (kDebugMode) {
        AppLogger.debug('NaverMapParser: 첫 번째 한글 텍스트 사용 - $searchQuery');
      }
    } else {
      throw NaverMapParseException('유효한 한글 텍스트를 찾을 수 없습니다');
    }

    // 2. 로컬 검색 API 호출
    if (kDebugMode) {
      AppLogger.debug('NaverMapParser: 로컬 검색 API 호출 - "$searchQuery"');
    }

    await Future.delayed(
      const Duration(milliseconds: _shortDelayMillis),
    ); // 429 에러 방지

    final searchResults = await _apiClient.searchLocal(
      query: searchQuery,
      latitude: userLatitude,
      longitude: userLongitude,
      display: 20, // 더 많은 결과 검색
    );

    if (searchResults.isEmpty) {
      throw NaverMapParseException('검색 결과가 없습니다: $searchQuery');
    }

    // 디버깅: 검색 결과 Place ID 분석
    if (kDebugMode) {
      AppLogger.debug('=== 로컬 검색 결과 Place ID 분석 ===');
      for (int i = 0; i < searchResults.length; i++) {
        final result = searchResults[i];
        final extractedId = result.extractPlaceId();
        AppLogger.debug('[$i] ${result.title}');
        AppLogger.debug('    링크: ${result.link}');
        AppLogger.debug('    추출된 Place ID: $extractedId (타겟: $placeId)');
      }
      AppLogger.debug('=====================================');
    }

    // 3. 최적의 결과 선택 - 3단계 매칭 알고리즘
    NaverLocalSearchResult? bestMatch;

    // 1차: Place ID가 정확히 일치하는 결과 찾기
    for (final result in searchResults) {
      final extractedId = result.extractPlaceId();
      if (extractedId == placeId) {
        bestMatch = result;
        if (kDebugMode) {
          AppLogger.debug('✅ 1차 매칭 성공: Place ID 일치 - ${result.title}');
        }
        break;
      }
    }

    // 2차: 상호명이 유사한 결과 찾기
    if (bestMatch == null) {
      // JSON-LD나 Apollo State에서 추출한 정확한 상호명이 있으면 사용
      String? exactName =
          koreanData['jsonLdName'] as String? ??
          koreanData['apolloStateName'] as String?;

      if (exactName != null) {
        for (final result in searchResults) {
          // 상호명 완전 일치 또는 포함 관계 확인
          if (result.title == exactName ||
              result.title.contains(exactName) ||
              exactName.contains(result.title)) {
            bestMatch = result;
            if (kDebugMode) {
              AppLogger.debug('✅ 2차 매칭 성공: 상호명 유사 - ${result.title}');
            }
            break;
          }
        }
      }
    }

    // 3차: 거리 기반 선택 (사용자 위치가 있는 경우)
    if (bestMatch == null && userLatitude != null && userLongitude != null) {
      bestMatch = _findNearestResult(
        searchResults,
        userLatitude,
        userLongitude,
      );
      if (bestMatch != null && kDebugMode) {
        AppLogger.debug('✅ 3차 매칭: 거리 기반 - ${bestMatch.title}');
      }
    }

    // 최종: 첫 번째 결과 사용
    if (bestMatch == null) {
      bestMatch = searchResults.first;
      if (kDebugMode) {
        AppLogger.debug('✅ 최종 매칭: 첫 번째 결과 사용 - ${bestMatch.title}');
      }
    }

    // 4. Restaurant 객체 생성
    final restaurant = bestMatch.toRestaurant(id: _uuid.v4());

    // 추가 정보 보완
    return restaurant.copyWith(
      naverPlaceId: placeId,
      naverUrl: finalUrl,
      source: DataSource.NAVER,
      updatedAt: DateTime.now(),
    );
  }

  /// 가장 가까운 결과 찾기 (거리 기반)
  NaverLocalSearchResult? _findNearestResult(
    List<NaverLocalSearchResult> results,
    double userLat,
    double userLng,
  ) {
    NaverLocalSearchResult? nearest;
    double minDistance = double.infinity;

    for (final result in results) {
      if (result.mapy != null && result.mapx != null) {
        // 네이버 좌표를 일반 좌표로 변환
        final lat = result.mapy! / _coordinateConversionFactor;
        final lng = result.mapx! / _coordinateConversionFactor;

        // 거리 계산
        final distance = _calculateDistance(userLat, userLng, lat, lng);

        if (distance < minDistance) {
          minDistance = distance;
          nearest = result;
        }
      }
    }

    if (kDebugMode && nearest != null) {
      AppLogger.debug(
        '가장 가까운 결과: ${nearest.title} (거리: ${minDistance.toStringAsFixed(2)}km)',
      );
    }

    return nearest;
  }

  /// 두 지점 간의 거리 계산 (Haversine 공식 사용)
  ///
  /// 반환값: 킬로미터 단위의 거리
  double _calculateDistance(
    double lat1,
    double lon1,
    double lat2,
    double lon2,
  ) {
    const double earthRadius = 6371.0; // 지구 반지름 (km)

    // 라디안으로 변환
    final double lat1Rad = lat1 * (3.141592653589793 / 180.0);
    final double lon1Rad = lon1 * (3.141592653589793 / 180.0);
    final double lat2Rad = lat2 * (3.141592653589793 / 180.0);
    final double lon2Rad = lon2 * (3.141592653589793 / 180.0);

    // 위도와 경도의 차이
    final double dLat = lat2Rad - lat1Rad;
    final double dLon = lon2Rad - lon1Rad;

    // Haversine 공식
    final double a =
        (sin(dLat / 2) * sin(dLat / 2)) +
        (cos(lat1Rad) * cos(lat2Rad) * sin(dLon / 2) * sin(dLon / 2));
    final double c = 2 * atan2(sqrt(a), sqrt(1 - a));

    return earthRadius * c;
  }

  /// 리소스 정리
  void dispose() {
    if (_isDisposed) return;
    _isDisposed = true;
    _apiClient.dispose();
  }
}

/// 네이버 지도 파싱 예외
class NaverMapParseException implements Exception {
  final String message;

  NaverMapParseException(this.message);

  @override
  String toString() => 'NaverMapParseException: $message';
}
