"""
Chrome 쿠키 관리 유틸리티.

- 자동 쿠키 파일 탐색 (SONOTE_COOKIES -> project cookies.txt -> output/data/cookies.txt)
- Chrome 쿠키 DB를 Netscape cookies.txt 형식으로 export
- cookies.txt 만료 상태 점검
"""

from __future__ import annotations

import base64
import ctypes
import hashlib
import json
import os
import shutil
import sqlite3
import sys
import tempfile
import time
from ctypes import wintypes
from pathlib import Path
from typing import Any

from .paths import OUTPUT_ROOT, data_dir, project_root

COOKIES_ENV_VAR = "SONOTE_COOKIES"
_FILE_HEADER = (
    "# Netscape HTTP Cookie File\n"
    "# This file was generated by sonote cookies export.\n"
)
_WINDOWS_EPOCH_OFFSET_SEC = 11644473600


def _expand_path(path: str | Path) -> Path:
    return Path(os.path.expandvars(str(path))).expanduser()


def _cookies_candidates() -> list[Path]:
    candidates: list[Path] = []
    env_path = os.getenv(COOKIES_ENV_VAR)
    if env_path:
        candidates.append(_expand_path(env_path))
    candidates.append(project_root() / "cookies.txt")
    candidates.append(OUTPUT_ROOT / "data" / "cookies.txt")
    return candidates


def resolve_cookies_path(
    cookies_path: str | Path | None = None,
    *,
    strict: bool = False,
) -> Path | None:
    """쿠키 파일 경로를 해석한다.

    Args:
        cookies_path: 명시 경로. None이면 자동 탐색.
        strict: True면 경로가 없을 때 예외 발생.
    """
    if cookies_path is not None:
        path = _expand_path(cookies_path)
        if path.exists() and path.is_file():
            return path.resolve()
        if strict:
            raise FileNotFoundError(f"쿠키 파일을 찾을 수 없습니다: {path}")
        return None

    for path in _cookies_candidates():
        if path.exists() and path.is_file():
            return path.resolve()
    return None


class _DATA_BLOB(ctypes.Structure):
    _fields_ = [
        ("cbData", wintypes.DWORD),
        ("pbData", ctypes.POINTER(ctypes.c_byte)),
    ]


def _dpapi_decrypt(data: bytes) -> bytes:
    if sys.platform != "win32":
        raise RuntimeError("DPAPI 복호화는 Windows에서만 지원됩니다.")
    if not data:
        return b""

    crypt32 = ctypes.windll.crypt32
    kernel32 = ctypes.windll.kernel32

    in_buf = ctypes.create_string_buffer(data, len(data))
    in_blob = _DATA_BLOB(
        cbData=len(data),
        pbData=ctypes.cast(in_buf, ctypes.POINTER(ctypes.c_byte)),
    )
    out_blob = _DATA_BLOB()

    ok = crypt32.CryptUnprotectData(
        ctypes.byref(in_blob),
        None,
        None,
        None,
        None,
        0,
        ctypes.byref(out_blob),
    )
    if not ok:
        err = ctypes.GetLastError()
        raise RuntimeError(f"DPAPI 복호화 실패 (code={err})")

    try:
        return ctypes.string_at(out_blob.pbData, out_blob.cbData)
    finally:
        if out_blob.pbData:
            kernel32.LocalFree(out_blob.pbData)


def _load_chrome_master_key(local_state_path: Path) -> tuple[bytes, bool]:
    if not local_state_path.exists():
        raise FileNotFoundError(f"Chrome Local State 파일이 없습니다: {local_state_path}")

    state = json.loads(local_state_path.read_text(encoding="utf-8"))
    os_crypt = state.get("os_crypt") or {}
    enc_key_b64 = os_crypt.get("encrypted_key")
    has_app_bound = bool(os_crypt.get("app_bound_encrypted_key"))
    if not enc_key_b64:
        raise RuntimeError("Local State에서 os_crypt.encrypted_key를 찾지 못했습니다.")

    encrypted_key = base64.b64decode(enc_key_b64)
    if encrypted_key.startswith(b"DPAPI"):
        encrypted_key = encrypted_key[5:]

    return _dpapi_decrypt(encrypted_key), has_app_bound


def _decrypt_aes_gcm(master_key: bytes, encrypted_value: bytes) -> bytes:
    try:
        from cryptography.hazmat.primitives.ciphers.aead import AESGCM
    except ImportError as exc:
        raise RuntimeError(
            "AES-GCM 복호화를 위해 'cryptography' 패키지가 필요합니다. "
            "설치 후 다시 시도하세요."
        ) from exc

    if len(encrypted_value) < 3 + 12 + 16:
        raise ValueError("AES-GCM 암호문 길이가 비정상입니다.")
    payload = encrypted_value[3:]
    nonce = payload[:12]
    ciphertext_and_tag = payload[12:]
    return AESGCM(master_key).decrypt(nonce, ciphertext_and_tag, None)


def _strip_host_hash_if_present(plaintext: bytes, host_key: str) -> bytes:
    if len(plaintext) < 32:
        return plaintext
    host_hash = hashlib.sha256(host_key.encode("utf-8")).digest()
    if plaintext.startswith(host_hash):
        return plaintext[32:]
    return plaintext


def _decrypt_cookie_value(
    value: str,
    encrypted_value: bytes,
    master_key: bytes,
    host_key: str,
) -> tuple[str | None, str]:
    """쿠키 값을 복호화한다.

    Returns:
        (cookie_value, status)
        status: plain | decrypted | v20 | error
    """
    if value:
        return value, "plain"

    if not encrypted_value:
        return "", "plain"

    if encrypted_value.startswith(b"v20"):
        return None, "v20"

    if encrypted_value.startswith((b"v10", b"v11")):
        try:
            plain = _decrypt_aes_gcm(master_key, encrypted_value)
            plain = _strip_host_hash_if_present(plain, host_key)
            return plain.decode("utf-8", errors="replace"), "decrypted"
        except RuntimeError:
            raise
        except Exception:
            return None, "error"

    try:
        # 구형 Chrome: encrypted_value 자체가 DPAPI
        plain = _dpapi_decrypt(encrypted_value)
        return plain.decode("utf-8", errors="replace"), "decrypted"
    except Exception:
        return None, "error"


def _chrome_expires_to_unix(expires_utc: int | float | None) -> int:
    if not expires_utc:
        return 0
    sec = int(expires_utc / 1_000_000 - _WINDOWS_EPOCH_OFFSET_SEC)
    return max(sec, 0)


def _sanitize_cookie_field(value: str) -> str:
    return value.replace("\t", "%09").replace("\r", "").replace("\n", "%0A")


def _format_netscape_line(
    host_key: str,
    path: str,
    is_secure: int | bool,
    expires_utc: int | float | None,
    name: str,
    value: str,
    is_httponly: int | bool,
) -> str:
    include_subdomains = "TRUE" if host_key.startswith(".") else "FALSE"
    secure_flag = "TRUE" if bool(is_secure) else "FALSE"
    expiry = str(_chrome_expires_to_unix(expires_utc))
    domain = f"#HttpOnly_{host_key}" if bool(is_httponly) else host_key
    safe_name = _sanitize_cookie_field(name)
    safe_value = _sanitize_cookie_field(value)
    safe_path = path or "/"
    return "\t".join(
        [domain, include_subdomains, safe_path, secure_flag, expiry, safe_name, safe_value]
    )


def _chrome_paths() -> tuple[Path, Path]:
    local_app_data = os.getenv("LOCALAPPDATA")
    if not local_app_data:
        raise RuntimeError("%LOCALAPPDATA% 환경변수가 없어 Chrome 경로를 찾을 수 없습니다.")
    chrome_root = Path(local_app_data) / "Google" / "Chrome" / "User Data"
    cookies_db = chrome_root / "Default" / "Network" / "Cookies"
    local_state = chrome_root / "Local State"
    return cookies_db, local_state


def export_chrome_cookies_to_netscape(output_path: str | Path | None = None) -> dict[str, Any]:
    """Chrome 쿠키를 Netscape cookies.txt 형식으로 export한다."""
    if sys.platform != "win32":
        raise RuntimeError("cookies export는 현재 Windows에서만 지원됩니다.")

    cookies_db, local_state = _chrome_paths()
    if not cookies_db.exists():
        raise FileNotFoundError(f"Chrome 쿠키 DB 파일이 없습니다: {cookies_db}")

    master_key, has_app_bound_key = _load_chrome_master_key(local_state)

    tmp_dir = Path(tempfile.mkdtemp(prefix="sonote-cookies-"))
    copied_db = tmp_dir / "Cookies"

    total = 0
    exported = 0
    skipped_v20 = 0
    skipped_error = 0
    lines: list[str] = [_FILE_HEADER]

    try:
        shutil.copy2(cookies_db, copied_db)
        conn = sqlite3.connect(copied_db)
        try:
            cur = conn.cursor()
            cur.execute(
                """
                SELECT host_key, path, is_secure, expires_utc, name, value, encrypted_value, is_httponly
                FROM cookies
                """
            )
            rows = cur.fetchall()
        finally:
            conn.close()

        for host_key, path, is_secure, expires_utc, name, value, enc_value, is_httponly in rows:
            total += 1
            enc_bytes = bytes(enc_value or b"")
            cookie_value, status = _decrypt_cookie_value(
                value or "",
                enc_bytes,
                master_key,
                host_key or "",
            )
            if status == "v20":
                skipped_v20 += 1
                continue
            if cookie_value is None:
                skipped_error += 1
                continue

            lines.append(
                _format_netscape_line(
                    host_key or "",
                    path or "/",
                    bool(is_secure),
                    expires_utc,
                    name or "",
                    cookie_value,
                    bool(is_httponly),
                )
            )
            exported += 1

        target = _expand_path(output_path) if output_path else (data_dir() / "cookies.txt")
        target.parent.mkdir(parents=True, exist_ok=True)
        target.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8")

        return {
            "output_path": str(target.resolve()),
            "source_db": str(cookies_db),
            "total_count": total,
            "exported_count": exported,
            "skipped_v20": skipped_v20,
            "skipped_error": skipped_error,
            "has_app_bound_key": has_app_bound_key,
        }
    finally:
        shutil.rmtree(tmp_dir, ignore_errors=True)


def check_cookies_file(cookies_path: str | Path | None = None) -> dict[str, Any]:
    """Netscape cookies.txt 파일의 만료 상태를 점검한다."""
    path = resolve_cookies_path(cookies_path, strict=bool(cookies_path))
    if path is None:
        return {
            "exists": False,
            "path": None,
            "total": 0,
            "valid": 0,
            "expired": 0,
            "session": 0,
            "malformed": 0,
        }

    now = int(time.time())
    total = 0
    valid = 0
    expired = 0
    session = 0
    malformed = 0

    for raw in path.read_text(encoding="utf-8", errors="replace").splitlines():
        line = raw.strip()
        if not line:
            continue
        if line.startswith("#") and not line.startswith("#HttpOnly_"):
            continue

        total += 1
        cols = raw.split("\t")
        if len(cols) < 7:
            malformed += 1
            continue

        try:
            expires_at = int(cols[4])
        except ValueError:
            malformed += 1
            continue

        if expires_at <= 0:
            session += 1
            valid += 1
        elif expires_at < now:
            expired += 1
        else:
            valid += 1

    return {
        "exists": True,
        "path": str(path),
        "total": total,
        "valid": valid,
        "expired": expired,
        "session": session,
        "malformed": malformed,
    }
