# -*- coding: utf-8 -*-
"""License activation against api.proxy.mindq.ru (product build)."""
from __future__ import annotations

import hashlib
import json
import os
import time
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from urllib import error, request

API_BASE = os.environ.get("CPC_API_BASE", "https://api.proxy.mindq.ru")
HOME = Path(os.environ["USERPROFILE"])
PRODUCT_DIR = Path(os.environ.get("CLAUDE_PROXY_HOME") or (HOME / ".claude-proxy-control"))
LICENSE_FILE = PRODUCT_DIR / "license.json"


def _machine_guid() -> str:
    """Идентификатор установки Windows. Удаление программы его не трогает."""
    try:
        import winreg

        with winreg.OpenKey(
            winreg.HKEY_LOCAL_MACHINE,
            r"SOFTWARE\Microsoft\Cryptography",
            0,
            winreg.KEY_READ | winreg.KEY_WOW64_64KEY,
        ) as key:
            value, _ = winreg.QueryValueEx(key, "MachineGuid")
        return str(value).strip()
    except Exception:
        return ""


def _volume_serial() -> str:
    """Серийный номер системного тома. Запасной признак машины."""
    try:
        import ctypes

        serial = ctypes.c_ulong(0)
        ok = ctypes.windll.kernel32.GetVolumeInformationW(
            ctypes.c_wchar_p(os.environ.get("SystemDrive", "C:") + "\\"),
            None, 0, ctypes.byref(serial), None, None, None, 0,
        )
        return f"{serial.value:08X}" if ok else ""
    except Exception:
        return ""


def device_id() -> str:
    """Постоянный идентификатор компьютера. Считается ИЗ МАШИНЫ, не из файла.

    Раньше это был случайный uuid в файле внутри папки продукта — а её сносит
    штатное удаление. Переустановка на ТОМ ЖЕ компьютере давала новый
    идентификатор, сервер видел вторую машину и отказывал:
    «device_limit:1:key_already_bound». Человек, переустановивший программу,
    оставался без собственной лицензии до ручной отвязки в кабинете.

    Поэтому никакого файла в основе. MachineGuid лежит в реестре Windows и
    переживает удаление программы, форматирование папки, что угодно кроме
    переустановки самой системы. Если реестр по какой-то причине недоступен —
    берём имя компьютера и серийный номер системного тома: тоже свойства
    машины, а не наши следы на ней.

    Наружу уходит только хеш: ни GUID, ни имя компьютера серверу знать незачем.
    """
    guid = _machine_guid()
    if guid:
        return "win-" + hashlib.sha256(f"cpc:{guid}".encode("utf-8")).hexdigest()[:16]

    serial = _volume_serial()
    name = os.environ.get("COMPUTERNAME", "")
    if serial or name:
        seed = f"cpc:{name}:{serial}"
        return "win-" + hashlib.sha256(seed.encode("utf-8")).hexdigest()[:16]

    # Совсем ничего не прочиталось — только тогда случайный, и записанный на
    # диск, чтобы он был стабилен хотя бы в пределах одной установки.
    PRODUCT_DIR.mkdir(parents=True, exist_ok=True)
    path = PRODUCT_DIR / "device_id.txt"
    if path.is_file():
        saved = path.read_text(encoding="utf-8").strip()
        if saved:
            return saved
    did = f"win-{uuid.uuid4().hex[:16]}"
    try:
        path.write_text(did, encoding="utf-8")
    except Exception:
        pass
    return did


def legacy_device_id() -> str:
    """Идентификатор из старой схемы, если файл ещё лежит на диске.

    Машины, активированные до перехода, привязаны на сервере к случайному
    значению. Отдаём его вместе с новым, чтобы сервер сам переписал привязку и
    человеку не пришлось ничего отвязывать руками.
    """
    path = PRODUCT_DIR / "device_id.txt"
    try:
        saved = path.read_text(encoding="utf-8").strip()
    except Exception:
        return ""
    return saved if saved and saved != device_id() else ""


def load_license() -> dict[str, Any] | None:
    if not LICENSE_FILE.is_file():
        return None
    try:
        return json.loads(LICENSE_FILE.read_text(encoding="utf-8"))
    except Exception:
        return None


def save_license(data: dict[str, Any]) -> None:
    PRODUCT_DIR.mkdir(parents=True, exist_ok=True)
    LICENSE_FILE.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")


def _activation_error(detail: str) -> str:
    """Человеческий текст вместо кода. Пользователь видел «device_limit:1:
    key_already_bound» и делал вывод, что ключ сломан."""
    low = detail.lower()
    if "key_already_bound" in low or "device_limit" in low:
        return (
            "Ключ уже привязан к другому компьютеру.\n\n"
            "Одна лицензия работает на одной машине. Чтобы перенести её сюда, "
            "зайдите в кабинет https://proxy.mindq.ru и нажмите «отвязать», "
            "затем активируйте ключ заново."
        )
    if "license_not_found" in low:
        return "Такого ключа нет. Проверьте, что он скопирован целиком."
    if "license_expired" in low:
        return "Срок лицензии истёк. Продлите её в кабинете и активируйте снова."
    if "license_inactive" in low:
        return "Лицензия отключена. Напишите в поддержку."
    if "device_revoked" in low:
        return "Этот компьютер был отвязан от ключа. Активируйте заново из кабинета."
    return f"Активация отклонена: {detail}"


def activate(license_key: str) -> dict[str, Any]:
    payload = json.dumps(
        {
            "license_key": license_key.strip(),
            "device_id": device_id(),
            # Чтобы сервер сам перевёл старую привязку на новый идентификатор.
            "legacy_device_id": legacy_device_id(),
            "app_version": "0.3.0-ctk",
        }
    ).encode("utf-8")
    req = request.Request(
        f"{API_BASE}/v1/license/activate",
        data=payload,
        headers={"Content-Type": "application/json"},
        method="POST",
    )
    try:
        with request.urlopen(req, timeout=30) as resp:
            data = json.loads(resp.read().decode("utf-8"))
    except error.HTTPError as e:
        body = e.read().decode("utf-8", errors="replace")
        try:
            detail = json.loads(body).get("detail", body)
        except Exception:
            detail = body
        raise RuntimeError(_activation_error(str(detail))) from e
    except Exception as e:
        raise RuntimeError(f"Нет связи с API: {e}") from e

    if not data.get("ok"):
        raise RuntimeError("Неожиданный ответ API")
    save_license(
        {
            "license_key": license_key.strip().upper(),
            "access_token": data.get("access_token"),
            "expires_at": data.get("expires_at"),
            "proxy": data.get("proxy"),
            "vpn_mode": data.get("vpn_mode", "sota_only"),
            "device_id": device_id(),
        }
    )
    proxy = data.get("proxy") or {}
    if proxy.get("host"):
        user = proxy.get("user", "proxy")
        password = proxy.get("password", "")
        host = proxy["host"]
        port = proxy.get("port", 13128)
        url = f"http://{user}:{password}@{host}:{port}"
        (PRODUCT_DIR / "proxy.env").write_text(
            f"HTTP_PROXY={url}\nHTTPS_PROXY={url}\nPROXY_HOST={host}\nPROXY_PORT={port}\n"
            f"PROXY_USER={user}\nPROXY_PASS={password}\n",
            encoding="utf-8",
        )
    return data


def is_licensed() -> bool:
    info = license_info()
    return bool(info.get("active"))


def open_magic_cabinet() -> str:
    """
    Ask API for a one-time cabinet login URL and return it.
    Raises RuntimeError on failure.
    """
    lic = load_license() or {}
    payload = json.dumps(
        {
            "license_key": lic.get("license_key") or "",
            "device_id": lic.get("device_id") or device_id(),
            "access_token": lic.get("access_token") or "",
        }
    ).encode("utf-8")
    req = request.Request(
        f"{API_BASE}/v1/billing/magic-cabinet",
        data=payload,
        headers={"Content-Type": "application/json"},
        method="POST",
    )
    try:
        with request.urlopen(req, timeout=30) as resp:
            data = json.loads(resp.read().decode("utf-8"))
    except error.HTTPError as e:
        body = e.read().decode("utf-8", errors="replace")
        try:
            detail = json.loads(body).get("detail", body)
        except Exception:
            detail = body
        raise RuntimeError(f"Не удалось открыть кабинет: {detail}") from e
    except Exception as e:
        raise RuntimeError(f"Нет связи с API: {e}") from e
    url = (data or {}).get("url") or ""
    if not url:
        raise RuntimeError("Сервер не вернул ссылку")
    return url


def license_info() -> dict[str, Any]:
    """UI-friendly license status for this device."""
    lic = load_license() or {}
    now = int(time.time())
    expires_at = int(lic.get("expires_at") or 0)
    has_key = bool(lic.get("license_key") and lic.get("access_token") and lic.get("proxy"))
    active = has_key and expires_at >= now
    days_left = max(0, (expires_at - now + 86399) // 86400) if expires_at else 0
    expires_human = ""
    if expires_at:
        expires_human = datetime.fromtimestamp(expires_at, tz=timezone.utc).strftime("%d.%m.%Y")
    proxy = lic.get("proxy") or {}
    proxy_host = str(proxy.get("host") or "").strip()
    proxy_port = proxy.get("port") or 13128
    proxy_ready = bool(
        has_key
        and proxy_host
        and (PRODUCT_DIR / "proxy.env").is_file()
    )
    return {
        "active": active,
        "has_key": has_key,
        "expired": has_key and expires_at < now,
        "license_key": lic.get("license_key") or "",
        "expires_at": expires_at,
        "expires_human": expires_human,
        "days_left": days_left,
        "device_id": lic.get("device_id") or device_id(),
        "proxy_host": proxy_host,
        "proxy_port": proxy_port,
        "proxy_ready": proxy_ready,
        "access_token": lic.get("access_token") or "",
    }


def send_heartbeat(tun_up: bool | None = None, app_version: str = "0.3.0-ctk") -> bool:
    """Best-effort heartbeat to License API. Returns True on success."""
    lic = load_license() or {}
    token = str(lic.get("access_token") or "")
    if not token:
        return False
    payload = json.dumps(
        {
            "device_id": lic.get("device_id") or device_id(),
            "tun_up": tun_up,
            "app_version": app_version,
        }
    ).encode("utf-8")
    req = request.Request(
        f"{API_BASE}/v1/heartbeat",
        data=payload,
        headers={
            "Content-Type": "application/json",
            "Authorization": f"Bearer {token}",
        },
        method="POST",
    )
    try:
        with request.urlopen(req, timeout=15) as resp:
            data = json.loads(resp.read().decode("utf-8"))
        return bool(data.get("ok", True))
    except Exception:
        return False
