"""Kakao Local API 기반 Geocoding + 오프셋 분산.

환경변수 KAKAO_REST_KEY 필요.
- access_pt 있는 구간: 주소/키워드 검색으로 정확한 좌표
- access_pt 없는 구간: sect_nm 기반 대표 좌표 + 나선형 오프셋
"""

from __future__ import annotations

import json
import math
import re
import time
from pathlib import Path
from typing import Optional, Tuple

import requests

import config

_HEADERS = {
    'Authorization': f'KakaoAK {config.KAKAO_REST_KEY}',
}

# Kakao API endpoints
_ADDRESS_URL = 'https://dapi.kakao.com/v2/local/search/address.json'
_KEYWORD_URL = 'https://dapi.kakao.com/v2/local/search/keyword.json'

# 캐시: query → (lat, lng) or None
_cache: dict[str, Optional[Tuple[float, float]]] = {}

# API 호출 간격 (초)
_RATE_LIMIT = 0.05


# ---------------------------------------------------------------------------
# Kakao API 호출
# ---------------------------------------------------------------------------
def _search_address(query: str) -> Optional[Tuple[float, float]]:
    """Kakao 주소 검색 API."""
    try:
        resp = requests.get(
            _ADDRESS_URL,
            params={'query': query},
            headers=_HEADERS,
            timeout=5,
        )
        if resp.status_code != 200:
            return None
        data = resp.json()
        docs = data.get('documents', [])
        if docs:
            return float(docs[0]['y']), float(docs[0]['x'])
    except Exception:
        pass
    return None


def _search_keyword(query: str) -> Optional[Tuple[float, float]]:
    """Kakao 키워드 검색 API."""
    try:
        resp = requests.get(
            _KEYWORD_URL,
            params={'query': query},
            headers=_HEADERS,
            timeout=5,
        )
        if resp.status_code != 200:
            return None
        data = resp.json()
        docs = data.get('documents', [])
        if docs:
            return float(docs[0]['y']), float(docs[0]['x'])
    except Exception:
        pass
    return None


# ---------------------------------------------------------------------------
# 메인 Geocoding 함수
# ---------------------------------------------------------------------------
def geocode(query: str) -> Optional[Tuple[float, float]]:
    """주소/키워드 → (lat, lng). 캐시 적용. 실패 시 None."""
    if not config.KAKAO_REST_KEY:
        return None

    if query in _cache:
        return _cache[query]

    time.sleep(_RATE_LIMIT)

    # 1차: 주소 검색 (지번/도로명)
    result = _search_address(query)

    # 2차: 키워드 검색 (장소명, 방조제, 해수욕장 등)
    if result is None:
        time.sleep(_RATE_LIMIT)
        result = _search_keyword(query)

    _cache[query] = result
    return result


def geocode_section(
    access_pt: Optional[str],
    sect_nm: str,
    zone_name: str,
) -> Optional[Tuple[float, float]]:
    """구간에 대한 최적 좌표 검색.

    우선순위:
    1. access_pt (주소/지명)
    2. sect_nm에서 '인근 해안' 제거한 지역명
    3. zone_name (PDF 구역명)
    """
    # 1. access_pt로 시도
    if access_pt:
        result = geocode(access_pt)
        if result:
            return result
        # access_pt + zone_name 조합 시도
        combined = f'{zone_name} {access_pt}' if zone_name else access_pt
        result = geocode(combined)
        if result:
            return result

    # 2. sect_nm에서 지역명 추출
    area = _extract_area(sect_nm)
    if area:
        result = geocode(area)
        if result:
            return result

    # 3. zone_name fallback
    if zone_name:
        result = geocode(zone_name)
        if result:
            return result

    return None


def _extract_area(sect_nm: str) -> str:
    """sect_nm에서 geocoding용 지역명 추출.

    예: '하동군 금남면 노량리 인근 해안' → '하동군 금남면 노량리'
    """
    if not sect_nm:
        return ''
    # '인근 해안' 등 제거
    area = re.sub(r'\s*인근\s*해안\s*$', '', sect_nm).strip()
    # '해안' 단독 제거
    area = re.sub(r'\s*해안\s*$', '', area).strip()
    return area


# ---------------------------------------------------------------------------
# 오프셋 분산 (같은 좌표에 겹치는 구간들 분산)
# ---------------------------------------------------------------------------
def apply_spiral_offset(
    base_lat: float,
    base_lng: float,
    index: int,
    spacing: float = 0.0005,
) -> Tuple[float, float]:
    """나선형 오프셋 적용. index=0이면 원점, 1부터 나선.

    spacing ~= 50m (위도 기준)
    """
    if index == 0:
        return base_lat, base_lng

    # 나선형: 각도와 반경 증가
    angle = index * 137.508 * math.pi / 180  # 황금각
    radius = spacing * math.sqrt(index)

    lat = base_lat + radius * math.cos(angle)
    lng = base_lng + radius * math.sin(angle)

    return round(lat, 6), round(lng, 6)


# ---------------------------------------------------------------------------
# 배치 Geocoding
# ---------------------------------------------------------------------------
def geocode_sections(
    sections: list[dict],
    zone_name: str = '',
) -> Tuple[int, int]:
    """섹션 리스트에 lat/lng를 채운다.

    Returns:
        (성공 수, 실패 수)
    """
    # sect_nm 그룹별로 처리 (오프셋 적용)
    from collections import defaultdict

    groups: dict[str, list[dict]] = defaultdict(list)
    for s in sections:
        groups[s.get('sect_nm', '')].append(s)

    success = 0
    fail = 0

    for sect_nm, group in groups.items():
        # 그룹 대표 좌표 구하기 (첫 번째 access_pt 있는 구간 또는 sect_nm)
        base_coord = None

        # access_pt가 있는 구간에서 먼저 시도
        for s in group:
            if s.get('access_pt'):
                coord = geocode_section(s['access_pt'], sect_nm, zone_name)
                if coord:
                    base_coord = coord
                    break

        # access_pt로 못 찾으면 sect_nm으로
        if base_coord is None:
            base_coord = geocode_section(None, sect_nm, zone_name)

        if base_coord is None:
            fail += len(group)
            continue

        # 그룹 내 구간별 좌표 할당
        # access_pt가 있는 구간은 개별 geocoding, 없으면 대표+오프셋
        offset_idx = 0
        for s in sorted(group, key=lambda x: x.get('section_number', 0)):
            if s.get('access_pt'):
                coord = geocode_section(s['access_pt'], sect_nm, zone_name)
                if coord:
                    s['lat'], s['lng'] = coord
                    success += 1
                    continue

            # 오프셋 적용
            lat, lng = apply_spiral_offset(base_coord[0], base_coord[1], offset_idx)
            s['lat'] = lat
            s['lng'] = lng
            offset_idx += 1
            success += 1

    return success, fail


# ---------------------------------------------------------------------------
# 캐시 저장/로드
# ---------------------------------------------------------------------------
_CACHE_FILE = Path(__file__).parent / 'output' / '.geocode_cache.json'


def save_cache():
    """캐시를 파일로 저장."""
    _CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
    serializable = {k: list(v) if v else None for k, v in _cache.items()}
    with open(_CACHE_FILE, 'w', encoding='utf-8') as f:
        json.dump(serializable, f, ensure_ascii=False, indent=2)


def load_cache():
    """캐시 파일 로드."""
    global _cache
    if _CACHE_FILE.exists():
        with open(_CACHE_FILE, encoding='utf-8') as f:
            data = json.load(f)
        _cache = {k: tuple(v) if v else None for k, v in data.items()}
