"""
离线 GeoIP 查询。纯本地运行，无网络请求。

三级存储：
1. 快速缓存 (data/geoip_cache.json) — 已查过的 IP，磁盘持久化
2. 离线数据库 (data/geoip_data.txt) — 手动整理的优先 IP 段
3. MMDB 数据库 — 若 python_geoip_geolite2 包已安装，加载其
   内置的 GeoLite2-City.mmdb 作为最全面回退。

查询链：缓存 > geoip_data.txt > MMDB > 写入缓存。
"""
import bisect
import json
import logging
import os
from pathlib import Path
from typing import Optional

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
#  Data structures
# ---------------------------------------------------------------------------

_GEO_DATA: list[tuple[int, int, float, float, str, str]] = []
_STARTS: list[int] = []
_RANGES: list[tuple[int, int, float, float, str, str]] = []

_MMDB_READER = None

_CACHE: dict[str, Optional[dict]] = {}
_CACHE_DIRTY = False
_CACHE_PATH = Path(__file__).resolve().parent.parent / 'data' / 'geoip_cache.json'


# ---------------------------------------------------------------------------
#  IP conversion
# ---------------------------------------------------------------------------

def ip_to_int(ip_str: str) -> Optional[int]:
    parts = ip_str.strip().split('.')
    if len(parts) != 4:
        return None
    try:
        octets = [int(p) for p in parts]
        if any(o < 0 or o > 255 for o in octets):
            return None
        return (octets[0] << 24) | (octets[1] << 16) | (octets[2] << 8) | octets[3]
    except (ValueError, TypeError):
        return None


# ---------------------------------------------------------------------------
#  geoip_data.txt loader
# ---------------------------------------------------------------------------

def _build_index():
    if not _GEO_DATA:
        return [], []
    sorted_data = sorted(_GEO_DATA, key=lambda x: x[0])
    starts = [entry[0] for entry in sorted_data]
    return starts, sorted_data


def _load_data_file() -> None:
    data_path = Path(__file__).resolve().parent.parent / 'data' / 'geoip_data.txt'
    if not data_path.exists():
        logger.warning('[GEOIP] data file not found: %s', data_path)
        return
    loaded = []
    skipped = 0
    with open(data_path, 'r', encoding='utf-8') as f:
        for raw in f:
            stripped = raw.strip()
            if not stripped or stripped.startswith('#'):
                continue
            parts = [p.strip() for p in stripped.split(',')]
            if len(parts) != 6:
                skipped += 1
                continue
            sip, eip, lat_s, lon_s, city, country = parts
            si = ip_to_int(sip)
            ei = ip_to_int(eip)
            if si is None or ei is None or si > ei:
                skipped += 1
                continue
            try:
                lat = float(lat_s)
                lon = float(lon_s)
            except ValueError:
                skipped += 1
                continue
            loaded.append((si, ei, lat, lon, city, country))
    _GEO_DATA.clear()
    _GEO_DATA.extend(loaded)
    global _STARTS, _RANGES
    _STARTS, _RANGES = _build_index()
    logger.info('[GEOIP] loaded %d ranges (%d skipped)', len(loaded), skipped)


# ---------------------------------------------------------------------------
#  MMDB reader (GeoLite2-City.mmdb bundled with python_geoip_geolite2)
# ---------------------------------------------------------------------------

def _get_mmdb_reader():
    global _MMDB_READER
    if _MMDB_READER is not None:
        return _MMDB_READER
    # Try multiple locations for the MMDB file
    candidates = []
    try:
        import _geoip_geolite2
        candidates.append(Path(_geoip_geolite2.__file__).parent / 'GeoLite2-City.mmdb')
    except Exception:
        pass
    # Fallback: temp directory without Chinese chars in path
    candidates.append(Path(os.environ.get('TEMP', 'C:/temp')) / 'opencode' / 'GeoLite2-City.mmdb')
    candidates.append(Path.home() / 'AppData' / 'Local' / 'Temp' / 'opencode' / 'GeoLite2-City.mmdb')

    for mmdb_path in candidates:
        if mmdb_path.exists():
            try:
                import maxminddb
                _MMDB_READER = maxminddb.open_database(mmdb_path)
                logger.info('[GEOIP] loaded MMDB: %s (%.1f MB)',
                            mmdb_path.name, mmdb_path.stat().st_size / 1e6)
                break
            except Exception as exc:
                logger.debug('[GEOIP] failed to open %s: %s', mmdb_path, exc)
                continue

    if _MMDB_READER is None:
        logger.debug('[GEOIP] MMDB not available — falling back to txt database only')
    return _MMDB_READER


def _mmdb_lookup(ip_str: str) -> Optional[dict]:
    reader = _get_mmdb_reader()
    if reader is None:
        return None
    try:
        result = reader.get(ip_str)
        if result is None:
            return None
        location = result.get('location', {})
        city_data = result.get('city', {})
        country_data = result.get('country', {})
        lat = location.get('latitude')
        lon = location.get('longitude')
        if lat is None or lon is None:
            return None
        city = ''
        if city_data and 'names' in city_data:
            city = city_data['names'].get('zh-CN') or city_data['names'].get('en', '')
        country = ''
        if country_data and 'names' in country_data:
            country = country_data['names'].get('zh-CN') or country_data['names'].get('en', '')
        return {'lat': float(lat), 'lon': float(lon), 'city': city, 'country': country}
    except Exception as exc:
        logger.debug('[GEOIP] MMDB lookup failed for %s: %s', ip_str, exc)
        return None


# ---------------------------------------------------------------------------
#  Disk cache
# ---------------------------------------------------------------------------

def _load_cache() -> None:
    if not _CACHE_PATH.exists():
        return
    try:
        data = json.loads(_CACHE_PATH.read_text(encoding='utf-8'))
        _CACHE.update(data)
        logger.info('[GEOIP] loaded %d cached entries', len(data))
    except Exception:
        pass


def _save_cache() -> None:
    global _CACHE_DIRTY
    if not _CACHE_DIRTY:
        return
    try:
        _CACHE_PATH.parent.mkdir(parents=True, exist_ok=True)
        _CACHE_PATH.write_text(
            json.dumps(_CACHE, ensure_ascii=False, indent=2),
            encoding='utf-8',
        )
        _CACHE_DIRTY = False
    except Exception as exc:
        logger.warning('[GEOIP] cache write failed: %s', exc)


# ---------------------------------------------------------------------------
#  Public API
# ---------------------------------------------------------------------------

def lookup(ip_str: str) -> Optional[dict]:
    """查询 IP 地理位置。纯本地，无网络。

    查询链：磁盘缓存 > geoip_data.txt (O(log N)) > MMDB (GeoLite2) > 写入缓存。
    """
    # 1. Cache hit
    cached = _CACHE.get(ip_str)
    if cached is not None:
        return cached
    if ip_str in _CACHE:
        return None

    result = None

    # 2. geoip_data.txt
    target = ip_to_int(ip_str)
    if target is not None and _STARTS:
        idx = bisect.bisect_right(_STARTS, target) - 1
        if idx >= 0:
            start, end, lat, lon, city, country = _RANGES[idx]
            if start <= target <= end:
                result = {'lat': lat, 'lon': lon, 'city': city, 'country': country}

    # 3. MMDB fallback (GeoLite2, ~3M entries world-wide)
    if result is None:
        result = _mmdb_lookup(ip_str)

    # 4. Cache for next time
    global _CACHE_DIRTY
    _CACHE[ip_str] = result
    _CACHE_DIRTY = True
    _save_cache()

    return result


# ---------------------------------------------------------------------------
#  Module-level init
# ---------------------------------------------------------------------------

_load_data_file()
_load_cache()
