"""
Local / hybrid server license binding — install and runtime checks.

A CNPERP LAN package must be activated with a valid CNP license before the
installer completes or the app serves ERP traffic (setup/licensing endpoints only).
"""
from __future__ import annotations

import json
import logging
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Dict, Optional

import httpx

from app.core.config import settings
from app.services.licensing_service import hash_license_key, normalize_license_key

logger = logging.getLogger(__name__)

_ACTIVATION_REL = Path("deploy/local-server/license-activation.json")
_NODE_REL = Path("deploy/license-node.json")
_CACHE: Dict[str, Any] = {"checked_at": None, "result": None}
_CACHE_SECONDS = 120


def _repo_root() -> Path:
    return Path(__file__).resolve().parents[2]


def activation_path() -> Path:
    return _repo_root() / _ACTIVATION_REL


def node_config_path() -> Path:
    custom = (settings.cloud_sync_config_path or "").strip()
    if custom:
        p = Path(custom)
        return p if p.is_absolute() else _repo_root() / p
    return _repo_root() / _NODE_REL


def is_local_deployment() -> bool:
    return (settings.deployment_mode or "cloud").lower() in ("local", "hybrid")


def load_activation() -> Optional[Dict[str, Any]]:
    path = activation_path()
    if not path.is_file():
        return None
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError):
        return None


def load_node_config() -> Dict[str, Any]:
    path = node_config_path()
    if not path.is_file():
        return {}
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError):
        return {}


def save_activation(data: Dict[str, Any]) -> Path:
    path = activation_path()
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
    return path


def save_node_config(data: Dict[str, Any]) -> Path:
    path = node_config_path()
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
    return path


def validate_license_with_cloud(license_key: str, deployment: str = "local") -> Dict[str, Any]:
    """Call cloud hub /licensing/validate (local installs always use cloud authority)."""
    cloud = (settings.license_cloud_url or "").strip().rstrip("/")
    if not cloud:
        return {
            "valid": False,
            "message": "LICENSE_CLOUD_URL is not configured on this server.",
        }
    key = normalize_license_key(license_key)
    try:
        with httpx.Client(timeout=25.0) as client:
            res = client.post(
                f"{cloud}/api/v1/licensing/validate",
                json={
                    "license_key": key,
                    "deployment": deployment,
                    "for_install": deployment == "local",
                },
            )
    except httpx.RequestError as exc:
        return {
            "valid": False,
            "message": f"Cannot reach CNP license server ({exc}). Internet required.",
        }
    if res.status_code >= 400:
        try:
            body = res.json()
            detail = body.get("detail") or body.get("message") or res.text
        except Exception:
            detail = res.text
        return {"valid": False, "message": str(detail) or "License validation failed."}
    data = res.json()
    if not data.get("valid"):
        return {"valid": False, "message": data.get("message", "Invalid license key.")}
    return data


def register_node_with_cloud(
    license_key: str,
    *,
    node_name: str,
    machine_fingerprint: str,
    public_base_url: Optional[str] = None,
    app_version: Optional[str] = None,
) -> Dict[str, Any]:
    cloud = (settings.license_cloud_url or "").strip().rstrip("/")
    if not cloud:
        return {"success": False, "message": "LICENSE_CLOUD_URL is not configured."}
    payload = {
        "license_key": normalize_license_key(license_key),
        "node_name": node_name,
        "machine_fingerprint": machine_fingerprint,
        "public_base_url": public_base_url or None,
        "app_version": app_version,
    }
    try:
        with httpx.Client(timeout=25.0) as client:
            res = client.post(f"{cloud}/api/v1/licensing/nodes/register", json=payload)
    except httpx.RequestError as exc:
        return {"success": False, "message": f"Cannot register node ({exc})."}
    if res.status_code >= 400:
        try:
            detail = res.json().get("detail", res.text)
        except Exception:
            detail = res.text
        return {"success": False, "message": str(detail)}
    return {"success": True, **res.json()}


def activate_local_server(
    license_key: str,
    *,
    cloud_url: Optional[str] = None,
    machine_fingerprint: Optional[str] = None,
    node_name: Optional[str] = None,
    public_base_url: Optional[str] = None,
) -> Dict[str, Any]:
    """
    Bind this CNPERP install to a license (install-time).
    Validates with cloud, registers deployment node, writes activation files.
    """
    from app.utils.machine_fingerprint import get_machine_fingerprint

    key = normalize_license_key(license_key)
    if not key:
        return {"success": False, "message": "License key is required."}

    if cloud_url:
        # Allow installer to pass cloud URL before .env exists
        import os
        os.environ["LICENSE_CLOUD_URL"] = cloud_url.rstrip("/")

    fp = machine_fingerprint or get_machine_fingerprint()
    name = node_name or socket_gethostname()

    check = validate_license_with_cloud(key, deployment="local")
    if not check.get("valid"):
        return {"success": False, "message": check.get("message", "License invalid.")}

    reg = register_node_with_cloud(
        key,
        node_name=name,
        machine_fingerprint=fp,
        public_base_url=public_base_url,
    )
    if not reg.get("success"):
        return {"success": False, "message": reg.get("message", "Node registration failed.")}

    now = datetime.now(timezone.utc).isoformat()
    key_hash = hash_license_key(key)

    node_cfg = {
        "cloud_url": (cloud_url or settings.license_cloud_url or "").rstrip("/"),
        "license_key": key,
        "node_name": name,
        "public_base_url": public_base_url or "",
        "machine_fingerprint": fp,
        "node_id": reg.get("node_id", ""),
        "sync_token": reg.get("sync_token", ""),
    }
    save_node_config(node_cfg)

    activation = {
        "key_hash": key_hash,
        "key_prefix": check.get("key_prefix") or key[:20] + "…",
        "machine_fingerprint": fp,
        "node_id": reg.get("node_id"),
        "activated_at": now,
        "cloud_validated_at": now,
        "valid_until": check.get("valid_until"),
        "plan": check.get("plan"),
    }
    save_activation(activation)
    _CACHE.clear()
    return {
        "success": True,
        "message": "License activated and bound to this server.",
        "key_prefix": activation["key_prefix"],
        "node_id": reg.get("node_id"),
    }


def socket_gethostname() -> str:
    import socket
    try:
        return socket.gethostname()
    except OSError:
        return "CNPERP-Local"


def local_server_license_status(force_refresh: bool = False) -> Dict[str, Any]:
    """Runtime check — is this local install licensed and bound to this machine?"""
    if not is_local_deployment():
        return {"active": True, "skipped": True, "reason": "not_local_deployment"}

    if not settings.license_enforcement:
        return {"active": True, "skipped": True, "reason": "enforcement_disabled"}

    now = datetime.now(timezone.utc)
    if not force_refresh and _CACHE.get("checked_at") and _CACHE.get("result"):
        checked = _CACHE["checked_at"]
        if (now - checked).total_seconds() < _CACHE_SECONDS:
            return _CACHE["result"]

    activation = load_activation()
    node = load_node_config()
    license_key = (node.get("license_key") or "").strip()

    if not activation or not license_key:
        result = {
            "active": False,
            "status": "unactivated",
            "message": (
                "This CNPERP local server is not licensed. "
                "Re-run the installer with your CNP license key."
            ),
        }
        _store_cache(now, result)
        return result

    from app.utils.machine_fingerprint import get_machine_fingerprint
    fp = get_machine_fingerprint()
    if activation.get("machine_fingerprint") and activation["machine_fingerprint"] != fp:
        result = {
            "active": False,
            "status": "wrong_machine",
            "message": (
                "This CNPERP install is licensed for a different computer. "
                "Contact CNP Solutions for a new license or reinstall on the licensed server."
            ),
        }
        _store_cache(now, result)
        return result

    if activation.get("key_hash") != hash_license_key(license_key):
        result = {
            "active": False,
            "status": "tampered",
            "message": "License configuration was modified. Re-activate with your installer.",
        }
        _store_cache(now, result)
        return result

    check = validate_license_with_cloud(license_key, deployment="local")
    if not check.get("valid"):
        result = {
            "active": False,
            "status": "invalid",
            "message": check.get("message", "License is no longer valid."),
        }
        _store_cache(now, result)
        return result

    activation["cloud_validated_at"] = now.isoformat()
    try:
        save_activation(activation)
    except OSError:
        pass

    result = {
        "active": True,
        "status": "active",
        "key_prefix": check.get("key_prefix") or activation.get("key_prefix"),
        "valid_until": check.get("valid_until"),
        "plan": check.get("plan"),
        "node_id": node.get("node_id"),
    }
    _store_cache(now, result)
    return result


def _store_cache(when: datetime, result: Dict[str, Any]) -> None:
    _CACHE["checked_at"] = when
    _CACHE["result"] = result
