"""
LicenseService - one-time online activation, hardware-bound, then offline.

The license key is exchanged once (online) for an opaque activation token bound
to this machine's id. The token is stored in the Windows Credential Manager
(never in settings.json). After activation the app runs offline; a best-effort
re-validation runs at startup but a network failure keeps the app activated
(offline grace) — only an explicit server "not activated" (403) revokes it.

Backend contract (server/app/main.py):
  POST /api/v1/activate {license_key, machine_id, machine_label?}
       -> 200 {ok:true, token} | 403/409 {ok:false, error}
  POST /api/v1/validate {token, machine_id}
       -> 200 {ok:true} | 403 {ok:false, error}
"""
from __future__ import annotations

import hashlib
import platform
from typing import TYPE_CHECKING

import httpx
from loguru import logger

from . import credentials
from .base_service import BaseService
from .http_exec import arequest
from .i18n import t

if TYPE_CHECKING:
    from .settings_service import SettingsService

API_BASE = "https://getvoxcast.com/api/v1"
_TOKEN_SECRET = "license_token"
_TIMEOUT = httpx.Timeout(20.0, connect=10.0)


def machine_id() -> str:
    """Stable, hardware/OS-bound id for this machine.

    Derived from the Windows MachineGuid (survives renames; changes on OS
    reinstall). Hashed so the raw guid never leaves the device in clear form.
    """
    guid = ""
    try:
        import winreg

        with winreg.OpenKey(
            winreg.HKEY_LOCAL_MACHINE,
            r"SOFTWARE\Microsoft\Cryptography",
            0,
            winreg.KEY_READ | winreg.KEY_WOW64_64KEY,
        ) as key:
            guid, _ = winreg.QueryValueEx(key, "MachineGuid")
    except OSError as e:
        logger.warning("MachineGuid unavailable ({}), falling back to node name", e)
        guid = platform.node() or "unknown"
    return hashlib.sha256(f"voxcast-machine:{guid}".encode("utf-8")).hexdigest()


def machine_label() -> str:
    """Human-readable label sent with activation (for the admin/account view)."""
    return (platform.node() or "PC")[:255]


def _app_version() -> str:
    """Current app version, sent so the server can enforce per-key version limits.

    Resilient to the frozen build where a bare ``import version`` may fail: try a
    couple of import paths and fall back to empty (the server treats empty as
    "no version info" and does not reject it)."""
    for mod in ("version", "src.version"):
        try:
            return str(__import__(mod, fromlist=["__version__"]).__version__)
        except Exception:
            continue
    return ""


def mask_key(key: str) -> str:
    """Mask a license key for display, keeping only the final group."""
    key = (key or "").strip().upper()
    parts = key.split("-")
    if len(parts) >= 2:
        return "-".join(["••••"] * (len(parts) - 1) + [parts[-1]])
    return "••••" + key[-4:] if len(key) > 4 else "••••"


class LicenseService(BaseService):
    def __init__(self, settings_service: SettingsService) -> None:
        self._settings = settings_service
        self._ready = False
        self._activated = False

    async def start(self) -> None:
        self._activated = credentials.has_secret(_TOKEN_SECRET)
        self._ready = True
        logger.info("LicenseService started (activated={})", self._activated)

    async def stop(self) -> None:
        self._ready = False

    @property
    def is_ready(self) -> bool:
        return self._ready

    def is_activated(self) -> bool:
        return self._activated

    async def activate(self, license_key: str) -> tuple[bool, str]:
        """Exchange a license key for a machine-bound token. Returns (ok, msg)."""
        license_key = (license_key or "").strip().upper()
        if len(license_key) < 4:
            return False, t("lic_enter_key")
        try:
            r = await arequest(
                "POST",
                f"{API_BASE}/activate",
                timeout=_TIMEOUT,
                json={
                    "license_key": license_key,
                    "machine_id": machine_id(),
                    "machine_label": machine_label(),
                    # None (omitted) rather than "" when the version can't be
                    # resolved — the server treats missing as "no version info".
                    "app_version": _app_version() or None,
                },
            )
            data = self._json(r)
            if r.status_code == 200 and data.get("ok") and data.get("token"):
                if not credentials.set_secret(_TOKEN_SECRET, data["token"]):
                    logger.error("Activation token could not be stored (Credential Manager)")
                    return False, t("lic_saved_failed")
                if self._settings.is_ready:
                    try:
                        self._settings.update(license_key_masked=mask_key(license_key))
                    except Exception as e:  # cosmetic only — don't fail activation
                        logger.warning("Could not persist masked key: {}", e)
                self._activated = True
                logger.info("License activated successfully")
                return True, t("lic_success")
            msg = data.get("error") or t("lic_check_failed")
            logger.warning("Activation rejected ({}): {}", r.status_code, msg)
            return False, msg
        except httpx.HTTPError as e:
            logger.error("Activation network error: {}", e)
            return False, t("lic_no_server")
        except Exception as e:  # never surface as the bridge's opaque catch-all
            logger.exception("Activation failed unexpectedly: {}", e)
            return False, t("lic_unexpected")

    async def validate(self) -> bool:
        """Best-effort re-validation. Network failure keeps activation (offline
        grace); only an explicit 403 revokes the stored token."""
        token = credentials.get_secret(_TOKEN_SECRET)
        if not token:
            self._activated = False
            return False
        try:
            r = await arequest(
                "POST",
                f"{API_BASE}/validate",
                timeout=_TIMEOUT,
                json={"token": token, "machine_id": machine_id()},
            )
            if r.status_code == 200 and self._json(r).get("ok"):
                self._activated = True
                return True
            if r.status_code == 403:
                logger.warning("License revoked by server — clearing local token")
                credentials.delete_secret(_TOKEN_SECRET)
                self._activated = False
                return False
            # Unexpected status — don't punish the user, keep activated.
            logger.warning("Unexpected validate status {}, keeping activation", r.status_code)
            self._activated = True
            return True
        except httpx.HTTPError as e:
            logger.info("Validate offline ({}); keeping activation", e)
            self._activated = True
            return True
        except Exception as e:
            # Never let an unexpected client error revoke a valid activation
            # (or crash startup) — fail open like a network error.
            logger.error("Validate failed unexpectedly ({}); keeping activation", e)
            self._activated = True
            return True

    def deactivate(self) -> None:
        """Remove the local activation (support/testing)."""
        credentials.delete_secret(_TOKEN_SECRET)
        if self._settings.is_ready:
            self._settings.update(license_key_masked=None)
        self._activated = False
        logger.info("License deactivated locally")

    @staticmethod
    def _json(r: httpx.Response) -> dict:
        try:
            data = r.json()
            return data if isinstance(data, dict) else {}
        except ValueError:
            return {}
