#!/usr/bin/env python3
"""
kis_approval_manager.py — KIS WebSocket approval_key 파일 캐시 (국내·해외 WS 공유)
==================================================================================
REST access_token(.kis_token_cache_*.json) 과 동일 패턴:
  - 프로세스·재시작·국내/해외 WS 가 **하나의 approval_key** 를 공유
  - KIS 정책: 24h 유효, 6h 이내 REST 재발급 금지
  - 파일 잠금으로 동시 발급(국내+해외·재시작) 경합 방지

문제(기존):
  - kis_ws / kis_ws_overseas 가 각각 /oauth2/Approval 호출 → 나중 키가 앞 키 무효화
  - 메모리만 사용 → 재시작마다 새 발급 → invalid approval 루프
"""
from __future__ import annotations

import json
import logging
import os
import threading
import time
from datetime import datetime
from pathlib import Path
from typing import Optional

import requests

logger = logging.getLogger(__name__)

ROOT = Path(__file__).parent
CACHE_MOCK = ROOT / ".kis_approval_cache_mock.json"
CACHE_REAL = ROOT / ".kis_approval_cache_real.json"
LOCK_FILE = ROOT / ".kis_approval_manager.lock"
LOCK_TIMEOUT_S = 60

DEFAULT_MIN_REISSUE_SEC = 21600.0
DEFAULT_MAX_VALID_SEC = 82800.0

_am_instances: dict = {}
_am_instances_lock = threading.Lock()


def _min_reissue_sec() -> float:
    try:
        raw = os.environ.get("KIS_WS_APPROVAL_MIN_REISSUE_SEC", "").strip()
        return max(0.0, float(raw)) if raw else DEFAULT_MIN_REISSUE_SEC
    except (ValueError, TypeError):
        return DEFAULT_MIN_REISSUE_SEC


def _max_valid_sec() -> float:
    try:
        raw = os.environ.get("KIS_WS_APPROVAL_MAX_VALID_SEC", "").strip()
        return max(3600.0, float(raw)) if raw else DEFAULT_MAX_VALID_SEC
    except (ValueError, TypeError):
        return DEFAULT_MAX_VALID_SEC


def _acquire_lock() -> bool:
    deadline = time.time() + LOCK_TIMEOUT_S
    while time.time() < deadline:
        try:
            fd = os.open(str(LOCK_FILE), os.O_CREAT | os.O_EXCL | os.O_WRONLY)
            os.write(fd, str(os.getpid()).encode())
            os.close(fd)
            return True
        except FileExistsError:
            try:
                if time.time() - LOCK_FILE.stat().st_mtime > 300:
                    LOCK_FILE.unlink(missing_ok=True)
                    continue
            except Exception:
                pass
            time.sleep(1)
        except Exception as exc:
            logger.warning("approval 잠금 획득 실패: %s", exc)
            return False
    logger.warning("approval 잠금 타임아웃 → 파일 캐시 재사용 시도")
    return False


def _release_lock() -> None:
    try:
        LOCK_FILE.unlink(missing_ok=True)
    except Exception:
        pass


def _issue_rest(app_key: str, app_secret: str, base_url: str) -> Optional[str]:
    url = f"{base_url.rstrip('/')}/oauth2/Approval"
    body = {
        "grant_type": "client_credentials",
        "appkey": app_key,
        "secretkey": app_secret,
    }
    try:
        resp = requests.post(url, json=body, timeout=10)
        data = resp.json()
        key = str(data.get("approval_key") or "").strip()
        if key:
            return key
        logger.error("❌ approval_key REST 발급 실패: %s", data)
    except Exception as exc:
        logger.error("❌ approval_key REST 요청 예외: %s", exc)
    return None


class KISApprovalManager:
    """KIS WebSocket approval_key 싱글톤 — 실전/모의 각 1개."""

    @classmethod
    def instance(cls, is_mock: bool) -> "KISApprovalManager":
        with _am_instances_lock:
            if is_mock not in _am_instances:
                _am_instances[is_mock] = cls(is_mock)
            return _am_instances[is_mock]

    def __init__(self, is_mock: bool) -> None:
        self._is_mock = bool(is_mock)
        self._mode_str = "모의" if is_mock else "실전"
        self._cache_path = CACHE_MOCK if is_mock else CACHE_REAL
        self._lock = threading.Lock()
        self._approval_key: Optional[str] = None
        self._issued_ts: float = 0.0
        self._app_key_prefix: str = ""
        self._load_from_file()

    def _load_from_file(self) -> None:
        if not self._cache_path.exists():
            return
        try:
            data = json.loads(self._cache_path.read_text(encoding="utf-8"))
            key = str(data.get("approval_key") or "").strip()
            ts = float(data.get("issued_at_epoch") or 0.0)
            prefix = str(data.get("app_key_prefix") or "")
            if not key or ts <= 0:
                return
            age = time.time() - ts
            if age >= _max_valid_sec():
                return
            self._approval_key = key
            self._issued_ts = ts
            self._app_key_prefix = prefix
        except Exception as exc:
            logger.debug("approval 파일 로드 실패 [%s]: %s", self._mode_str, exc)

    def _save_to_file(self) -> None:
        if not self._approval_key or self._issued_ts <= 0:
            return
        try:
            self._cache_path.write_text(
                json.dumps(
                    {
                        "approval_key": self._approval_key,
                        "issued_at": datetime.fromtimestamp(self._issued_ts).strftime(
                            "%Y-%m-%d %H:%M:%S"
                        ),
                        "issued_at_epoch": self._issued_ts,
                        "app_key_prefix": self._app_key_prefix,
                        "mock": self._is_mock,
                    },
                    ensure_ascii=False,
                    indent=2,
                ),
                encoding="utf-8",
            )
        except Exception as exc:
            logger.warning("approval 파일 저장 실패 (%s): %s", self._cache_path.name, exc)

    def age_sec(self) -> float:
        if not self._issued_ts:
            return 999999.0
        return max(0.0, time.time() - self._issued_ts)

    def reload_from_file(self) -> Optional[str]:
        """REST 없이 파일 캐시만 다시 읽기 (다른 프로세스 갱신 반영)."""
        with self._lock:
            self._load_from_file()
            return self._approval_key

    @property
    def issued_ts(self) -> float:
        return self._issued_ts

    def get_approval_key(
        self,
        app_key: str,
        app_secret: str,
        base_url: str,
        *,
        force_refresh: bool = False,
    ) -> Optional[str]:
        """
        유효한 approval_key 반환.
        - 24h 이내: 파일/메모리 캐시 재사용 (재연결 포함)
        - 6h 미경과: force_refresh=True 여도 REST 재발급 금지
        - 6h 경과 + 24h 미만 + force_refresh: REST 재발급 허용
        - 24h 경과: REST 재발급
        """
        app_key = (app_key or "").strip()
        app_secret = (app_secret or "").strip()
        if not app_key or not app_secret:
            return None

        min_reissue = _min_reissue_sec()
        max_valid = _max_valid_sec()

        with self._lock:
            self._load_from_file()
            age = self.age_sec()
            prefix_ok = (
                not self._app_key_prefix
                or app_key.startswith(self._app_key_prefix)
                or self._app_key_prefix.startswith(app_key[:8])
            )
            if self._approval_key and age < max_valid and prefix_ok:
                if force_refresh and age >= min_reissue:
                    pass
                elif force_refresh and age < min_reissue:
                    logger.info(
                        "🔑 approval_key REST 재발급 생략 (KIS 6h — %.0f분/%.0fh, %s 재사용)",
                        age / 60,
                        min_reissue / 3600,
                        self._cache_path.name,
                    )
                    return self._approval_key
                else:
                    return self._approval_key

        if not _acquire_lock():
            with self._lock:
                self._load_from_file()
                return self._approval_key

        try:
            with self._lock:
                self._load_from_file()
                age = self.age_sec()
                if self._approval_key and age < max_valid:
                    if not force_refresh or age < min_reissue:
                        return self._approval_key

            key = _issue_rest(app_key, app_secret, base_url)
            if not key:
                with self._lock:
                    return self._approval_key

            now = time.time()
            with self._lock:
                self._approval_key = key
                self._issued_ts = now
                self._app_key_prefix = app_key[:8]
                self._save_to_file()
            logger.info(
                "✅ WebSocket approval_key 발급·저장 (%s, 앞8자: %s…, "
                "다음 REST 가능: %.0fh 후 → %s)",
                self._mode_str,
                key[:8],
                min_reissue / 3600,
                self._cache_path.name,
            )
            return key
        finally:
            _release_lock()


def get_approval_status(is_mock: bool) -> dict:
    mgr = KISApprovalManager.instance(is_mock)
    with mgr._lock:
        mgr._load_from_file()
        age_h = round(mgr.age_sec() / 3600.0, 2) if mgr._approval_key else -1.0
        valid = bool(mgr._approval_key) and mgr.age_sec() < _max_valid_sec()
        return {
            "valid": valid,
            "key_prefix": (mgr._approval_key[:8] + "…") if mgr._approval_key else "",
            "age_h": age_h,
            "file": str(mgr._cache_path.name),
        }
