"""
License key generation, validation, activation, renewal, and enforcement.
"""
from __future__ import annotations

import hashlib
import re
import secrets
import uuid
from datetime import datetime, timedelta
from typing import Any, Dict, Optional, Tuple

import httpx
from jose import JWTError, jwt
from sqlalchemy.orm import Session

from app.core.config import settings
from app.models.company import Company
from app.models.branch import Branch
from app.models.licensing import BranchLicense, DeploymentNode, LicenseKey

LICENSE_KEY_PATTERN = re.compile(
    r"^CNP-[A-Z0-9]{4}-\d{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$"
)


def normalize_license_key(raw: str) -> str:
    key = (raw or "").strip().upper().replace(" ", "")
    # Allow paste without dashes: CNPSTND2026ABCD...
    if key.startswith("CNP") and key.count("-") < 5:
        key = key.replace("CNP", "CNP-", 1)
    return key


def hash_license_key(key: str) -> str:
    secret = settings.license_signing_secret or settings.secret_key
    payload = f"{normalize_license_key(key)}:{secret}"
    return hashlib.sha256(payload.encode("utf-8")).hexdigest()


def encrypt_license_key_for_storage(full_key: str) -> str:
    """Store reversible copy for CNP platform admin (JWT, server secret only)."""
    secret = settings.license_signing_secret or settings.secret_key
    return jwt.encode({"license_key": normalize_license_key(full_key)}, secret, algorithm="HS256")


def decrypt_license_key_from_storage(encrypted: Optional[str]) -> Optional[str]:
    if not encrypted:
        return None
    try:
        secret = settings.license_signing_secret or settings.secret_key
        payload = jwt.decode(encrypted, secret, algorithms=["HS256"])
        return payload.get("license_key")
    except JWTError:
        return None


def uses_cloud_license_authority() -> bool:
    """Local/hybrid installs must validate keys against the CNP cloud hub."""
    if settings.license_allow_offline_setup:
        return False
    dep = (settings.deployment_mode or "cloud").lower()
    return dep in ("local", "hybrid")


def _signing_secret() -> str:
    return settings.license_signing_secret or settings.secret_key


def create_setup_token(raw_key: str, deployment: str) -> str:
    """Short-lived proof of successful license validation (issued by cloud hub)."""
    minutes = max(5, int(settings.license_setup_token_minutes or 45))
    payload = {
        "typ": "cnp_setup",
        "key_hash": hash_license_key(raw_key),
        "deployment": (deployment or "cloud").lower(),
        "exp": datetime.utcnow() + timedelta(minutes=minutes),
    }
    return jwt.encode(payload, _signing_secret(), algorithm="HS256")


def verify_setup_token(raw_key: str, token: str, deployment: Optional[str] = None) -> bool:
    if not token:
        return False
    try:
        data = jwt.decode(token, _signing_secret(), algorithms=["HS256"])
    except JWTError:
        return False
    if data.get("typ") != "cnp_setup":
        return False
    if data.get("key_hash") != hash_license_key(raw_key):
        return False
    if deployment and data.get("deployment") != deployment.lower():
        return False
    return True


def create_mirror_provision_token(company_id: str, license_key: str) -> str:
    """LAN admin — authorize cloud mirror provisioning for an existing local company."""
    minutes = max(5, int(settings.license_setup_token_minutes or 45))
    payload = {
        "typ": "cnp_mirror_provision",
        "company_id": company_id,
        "key_hash": hash_license_key(license_key),
        "exp": datetime.utcnow() + timedelta(minutes=minutes),
    }
    return jwt.encode(payload, _signing_secret(), algorithm="HS256")


def verify_mirror_provision_token(company_id: str, license_key: str, token: str) -> bool:
    if not token:
        return False
    try:
        data = jwt.decode(token, _signing_secret(), algorithms=["HS256"])
    except JWTError:
        return False
    if data.get("typ") != "cnp_mirror_provision":
        return False
    if data.get("company_id") != company_id:
        return False
    if data.get("key_hash") != hash_license_key(license_key):
        return False
    return True


def generate_license_key(plan: str = "standard", year: Optional[int] = None) -> Tuple[str, str, str]:
    """Returns (full_key, key_hash, key_prefix_for_display)."""
    year = year or datetime.utcnow().year
    plan_tag = re.sub(r"[^A-Z0-9]", "", (plan or "STND").upper())[:4].ljust(4, "X")
    segments = [secrets.token_hex(2).upper() for _ in range(3)]
    full_key = f"CNP-{plan_tag}-{year}-{'-'.join(segments)}"
    key_hash = hash_license_key(full_key)
    key_prefix = f"CNP-{plan_tag}-{year}-****-****-{segments[-1]}"
    return full_key, key_hash, key_prefix


class LicensingService:
    def __init__(self, db: Session):
        self.db = db

    def find_by_key(self, raw_key: str) -> Optional[LicenseKey]:
        h = hash_license_key(raw_key)
        return self.db.query(LicenseKey).filter(LicenseKey.key_hash == h).first()

    def validate_for_setup(
        self,
        raw_key: str,
        deployment: str = "cloud",
    ) -> Dict[str, Any]:
        if uses_cloud_license_authority():
            return self._validate_for_setup_via_cloud(raw_key, deployment)
        return self._validate_for_setup_local(raw_key, deployment)

    def _validate_for_setup_local(
        self,
        raw_key: str,
        deployment: str = "cloud",
    ) -> Dict[str, Any]:
        key = normalize_license_key(raw_key)
        if not LICENSE_KEY_PATTERN.match(key):
            return {
                "valid": False,
                "message": "Invalid license format. Expected: CNP-PLAN-YYYY-XXXX-XXXX-XXXX",
            }

        record = self.find_by_key(key)
        if not record:
            return {"valid": False, "message": "License key not found. Check the code from CNP Solutions."}

        blocked = self._check_license_record_blocks(record, deployment)
        if blocked:
            return blocked

        now = datetime.utcnow()
        dep = (deployment or "cloud").lower()
        days_left = max(0, (record.valid_until - now).days)
        result = {
            "valid": True,
            "license_id": record.id,
            "plan": record.plan,
            "deployment_mode": record.deployment_mode,
            "valid_from": record.valid_from.isoformat(),
            "valid_until": record.valid_until.isoformat(),
            "days_remaining": days_left,
            "max_users": record.max_users,
            "max_branches": record.max_branches,
            "key_prefix": record.key_prefix,
            "status": record.status,
        }
        if not uses_cloud_license_authority():
            result["setup_token"] = create_setup_token(raw_key, dep)
        return result

    def _validate_for_setup_via_cloud(
        self,
        raw_key: str,
        deployment: str = "cloud",
    ) -> Dict[str, Any]:
        cloud = (settings.license_cloud_url or "").strip().rstrip("/")
        if not cloud:
            return {
                "valid": False,
                "message": (
                    "This server is not linked to the CNP license hub. "
                    "Set LICENSE_CLOUD_URL or reinstall using the official installer."
                ),
            }

        dep = (deployment or settings.deployment_mode or "local").lower()
        url = f"{cloud}/api/v1/licensing/validate"
        try:
            with httpx.Client(timeout=20.0) as client:
                response = client.post(
                    url,
                    json={"license_key": raw_key, "deployment": dep},
                )
        except httpx.RequestError as exc:
            return {
                "valid": False,
                "message": (
                    "Cannot reach the CNP license server. Connect this PC to the internet "
                    f"and try again. ({exc})"
                ),
            }

        if response.status_code >= 400:
            try:
                detail = response.json().get("detail", response.text)
            except Exception:
                detail = response.text
            return {"valid": False, "message": str(detail) or "License validation failed."}

        data = response.json()
        if not data.get("valid"):
            return data

        self._sync_license_stub_from_cloud(raw_key, data)
        return data

    def _check_license_record_blocks(
        self,
        record: LicenseKey,
        deployment: str,
    ) -> Optional[Dict[str, Any]]:
        if record.status == "revoked":
            return {"valid": False, "message": "This license has been revoked. Contact CNP Solutions."}

        if record.status == "suspended":
            return {"valid": False, "message": "This license is suspended. Contact CNP Solutions."}

        now = datetime.utcnow()
        if record.valid_until < now:
            return {
                "valid": False,
                "message": "License has expired. Request a renewal from CNP Solutions.",
                "expired_at": record.valid_until.isoformat(),
            }

        if record.status == "active" and record.activated_at:
            if record.company_id:
                return {
                    "valid": False,
                    "message": "This license is already activated on another company.",
                }
            return {
                "valid": False,
                "message": "This license is already activated on another server. Contact CNP Solutions.",
            }

        dep = (deployment or "cloud").lower()
        mode = (record.deployment_mode or "both").lower()
        if mode != "both" and mode != dep:
            return {
                "valid": False,
                "message": f"This license is for {mode} deployment only, not {dep}.",
            }
        return None

    def _sync_license_stub_from_cloud(self, raw_key: str, cloud_data: Dict[str, Any]) -> LicenseKey:
        """Cache cloud license metadata locally so activation can complete offline briefly."""
        key = normalize_license_key(raw_key)
        h = hash_license_key(key)
        record = self.db.query(LicenseKey).filter(LicenseKey.key_hash == h).first()
        valid_until = cloud_data.get("valid_until")
        if isinstance(valid_until, str):
            valid_until_dt = datetime.fromisoformat(valid_until.replace("Z", "+00:00").replace("+00:00", ""))
        else:
            valid_until_dt = datetime.utcnow() + timedelta(days=365)

        valid_from = cloud_data.get("valid_from")
        if isinstance(valid_from, str):
            valid_from_dt = datetime.fromisoformat(valid_from.replace("Z", "+00:00").replace("+00:00", ""))
        else:
            valid_from_dt = datetime.utcnow()

        if not record:
            record = LicenseKey(
                id=str(uuid.uuid4()),
                key_hash=h,
                key_prefix=cloud_data.get("key_prefix") or key[:20],
                plan=cloud_data.get("plan") or "standard",
                deployment_mode=cloud_data.get("deployment_mode") or "both",
                status=cloud_data.get("status") or "pending",
                valid_from=valid_from_dt,
                valid_until=valid_until_dt,
                max_users=cloud_data.get("max_users") or 25,
                max_branches=cloud_data.get("max_branches") or 10,
            )
            self.db.add(record)
        else:
            record.key_prefix = cloud_data.get("key_prefix") or record.key_prefix
            record.plan = cloud_data.get("plan") or record.plan
            record.deployment_mode = cloud_data.get("deployment_mode") or record.deployment_mode
            record.valid_from = valid_from_dt
            record.valid_until = valid_until_dt
            record.max_users = cloud_data.get("max_users") or record.max_users
            record.max_branches = cloud_data.get("max_branches") or record.max_branches
            if cloud_data.get("status"):
                record.status = cloud_data["status"]

        self.db.flush()
        return record

    def verify_setup_with_cloud(self, raw_key: str, setup_token: str, deployment: str) -> Dict[str, Any]:
        cloud = (settings.license_cloud_url or "").strip().rstrip("/")
        if not cloud:
            return {"valid": False, "message": "License hub URL is not configured."}
        url = f"{cloud}/api/v1/licensing/verify-setup"
        try:
            with httpx.Client(timeout=15.0) as client:
                response = client.post(
                    url,
                    json={
                        "license_key": raw_key,
                        "setup_token": setup_token,
                        "deployment": deployment,
                    },
                )
            return response.json()
        except httpx.RequestError as exc:
            return {"valid": False, "message": f"Cannot reach license server: {exc}"}

    def claim_setup_on_cloud(
        self,
        raw_key: str,
        setup_token: str,
        deployment: str,
        site_name: Optional[str] = None,
    ) -> Dict[str, Any]:
        cloud = (settings.license_cloud_url or "").strip().rstrip("/")
        if not cloud:
            return {"success": False, "message": "License hub URL is not configured."}
        url = f"{cloud}/api/v1/licensing/claim-setup"
        try:
            with httpx.Client(timeout=15.0) as client:
                response = client.post(
                    url,
                    json={
                        "license_key": raw_key,
                        "setup_token": setup_token,
                        "deployment": deployment,
                        "site_name": site_name,
                    },
                )
            if response.status_code >= 400:
                return {"success": False, "message": response.text}
            return response.json()
        except httpx.RequestError as exc:
            return {"success": False, "message": str(exc)}

    def activate_for_company(
        self,
        raw_key: str,
        company: Company,
        activated_by_email: Optional[str] = None,
        deployment: str = "cloud",
    ) -> LicenseKey:
        validation = self.validate_for_setup(raw_key, deployment=deployment)
        if not validation.get("valid"):
            raise ValueError(validation.get("message", "Invalid license"))

        record = self.find_by_key(raw_key)
        if not record:
            raise ValueError("License key not found")

        now = datetime.utcnow()
        record.status = "active"
        record.company_id = company.id
        record.activated_at = now
        record.activated_by_email = activated_by_email

        company.license_key_id = record.id
        company.license_expires_at = record.valid_until
        company.deployment_mode = deployment if deployment in ("cloud", "local", "hybrid") else "cloud"
        company.subscription_plan = record.plan or company.subscription_plan

        self.db.flush()
        return record

    def issue_license(
        self,
        *,
        plan: str = "standard",
        years: int = 1,
        deployment_mode: str = "both",
        customer_name: Optional[str] = None,
        customer_email: Optional[str] = None,
        max_users: int = 25,
        max_branches: int = 1,
        notes: Optional[str] = None,
    ) -> Tuple[LicenseKey, str]:
        full_key, key_hash, key_prefix = generate_license_key(plan=plan)
        now = datetime.utcnow()
        term_years = max(1, min(int(years or 1), 10))
        valid_until = now + timedelta(days=365 * term_years)

        record = LicenseKey(
            id=str(uuid.uuid4()),
            key_prefix=key_prefix,
            key_hash=key_hash,
            key_encrypted=encrypt_license_key_for_storage(full_key),
            plan=plan,
            deployment_mode=deployment_mode,
            status="pending",
            valid_from=now,
            valid_until=valid_until,
            max_users=max_users,
            max_branches=max_branches,
            customer_name=customer_name,
            customer_email=customer_email,
            notes=notes,
        )
        self.db.add(record)
        self.db.flush()
        return record, full_key

    def renew_license(self, license_id: str, years: int = 1) -> LicenseKey:
        record = self.db.query(LicenseKey).filter(LicenseKey.id == license_id).first()
        if not record:
            raise ValueError("License not found")

        now = datetime.utcnow()
        base = record.valid_until if record.valid_until > now else now
        term_years = max(1, min(int(years or 1), 10))
        record.valid_until = base + timedelta(days=365 * term_years)
        record.status = "active"
        record.renewals_count = (record.renewals_count or 0) + 1

        if record.company_id:
            company = self.db.query(Company).filter(Company.id == record.company_id).first()
            if company:
                company.license_expires_at = record.valid_until

        self._sync_branch_licenses_to_parent(record)
        self.db.flush()
        return record

    def upgrade_license(
        self,
        license_id: str,
        *,
        max_branches: Optional[int] = None,
        max_users: Optional[int] = None,
        add_years: int = 0,
    ) -> LicenseKey:
        """Increase branch/user limits and/or extend expiry on a main license."""
        record = self.db.query(LicenseKey).filter(LicenseKey.id == license_id).first()
        if not record:
            raise ValueError("License not found")
        if record.status == "revoked":
            raise ValueError("Cannot upgrade a revoked license — issue a new key")

        changed = False
        if max_branches is not None:
            if max_branches < 1:
                raise ValueError("max_branches must be at least 1")
            record.max_branches = max_branches
            changed = True
        if max_users is not None:
            if max_users < 1:
                raise ValueError("max_users must be at least 1")
            record.max_users = max_users
            changed = True

        extra_years = max(0, min(int(add_years or 0), 10))
        if extra_years > 0:
            now = datetime.utcnow()
            base = record.valid_until if record.valid_until > now else now
            record.valid_until = base + timedelta(days=365 * extra_years)
            record.status = "active"
            changed = True
            if record.company_id:
                company = self.db.query(Company).filter(Company.id == record.company_id).first()
                if company:
                    company.license_expires_at = record.valid_until
            self._sync_branch_licenses_to_parent(record)

        if not changed:
            raise ValueError("Specify at least one change: max_branches, max_users, or add_years")

        self.db.flush()
        return record

    def revoke_license(self, license_id: str) -> LicenseKey:
        record = self.db.query(LicenseKey).filter(LicenseKey.id == license_id).first()
        if not record:
            raise ValueError("License not found")
        record.status = "revoked"
        self._revoke_branch_licenses_for_parent(record.id)
        self.db.flush()
        return record

    MAIN_INCLUDES_BRANCHES = 1

    def count_active_branch_licenses(self, company_id: str) -> int:
        now = datetime.utcnow()
        return (
            self.db.query(BranchLicense)
            .join(LicenseKey, BranchLicense.parent_license_id == LicenseKey.id)
            .filter(
                BranchLicense.company_id == company_id,
                BranchLicense.status == "active",
                BranchLicense.valid_until >= now,
                LicenseKey.status.in_(("pending", "active")),
                LicenseKey.valid_until >= now,
            )
            .count()
        )

    def _sync_branch_licenses_to_parent(self, parent: LicenseKey) -> None:
        branch_lics = (
            self.db.query(BranchLicense)
            .filter(
                BranchLicense.parent_license_id == parent.id,
                BranchLicense.status == "active",
            )
            .all()
        )
        for bl in branch_lics:
            bl.valid_until = parent.valid_until
            if parent.valid_from:
                bl.valid_from = parent.valid_from

    def _revoke_branch_licenses_for_parent(self, parent_license_id: str) -> int:
        branch_lics = (
            self.db.query(BranchLicense)
            .filter(
                BranchLicense.parent_license_id == parent_license_id,
                BranchLicense.status == "active",
            )
            .all()
        )
        for bl in branch_lics:
            bl.status = "revoked"
            bl.branch_id = None
        return len(branch_lics)

    def issue_branch_license(
        self,
        company_id: str,
        *,
        label: Optional[str] = None,
        branch_id: Optional[str] = None,
        notes: Optional[str] = None,
    ) -> BranchLicense:
        company = self.db.query(Company).filter(Company.id == company_id).first()
        if not company:
            raise ValueError("Company not found")
        if not company.license_key_id:
            raise ValueError("Company has no main license. Assign or activate a main license first.")

        parent = (
            self.db.query(LicenseKey).filter(LicenseKey.id == company.license_key_id).first()
        )
        if not parent:
            raise ValueError("Main license record not found")
        if parent.status == "revoked":
            raise ValueError("Main license is revoked — renew or issue a new main license first.")

        if branch_id:
            branch = self.db.query(Branch).filter(
                Branch.id == branch_id, Branch.company_id == company_id
            ).first()
            if not branch:
                raise ValueError("Branch not found for this company")
            existing = (
                self.db.query(BranchLicense)
                .filter(
                    BranchLicense.branch_id == branch_id,
                    BranchLicense.status == "active",
                )
                .first()
            )
            if existing:
                raise ValueError("This branch already has an active branch license slot")

        now = datetime.utcnow()
        seq = (
            self.db.query(BranchLicense)
            .filter(BranchLicense.parent_license_id == parent.id)
            .count()
            + 1
        )
        code_base = re.sub(r"[^A-Z0-9]", "", (company.company_code or company.id[:6]).upper())[:8]
        slot_code = f"BR-{code_base}-{seq:03d}"

        bl = BranchLicense(
            id=str(uuid.uuid4()),
            parent_license_id=parent.id,
            company_id=company_id,
            branch_id=branch_id,
            slot_code=slot_code,
            label=label or f"Additional branch #{seq}",
            status="active",
            valid_from=parent.valid_from or now,
            valid_until=parent.valid_until,
            notes=notes,
        )
        self.db.add(bl)
        self.db.flush()
        return bl

    def revoke_branch_license(self, branch_license_id: str) -> BranchLicense:
        bl = self.db.query(BranchLicense).filter(BranchLicense.id == branch_license_id).first()
        if not bl:
            raise ValueError("Branch license not found")
        bl.status = "revoked"
        bl.branch_id = None
        self.db.flush()
        return bl

    def get_max_branches_for_company(self, company_id: str) -> int:
        company = self.db.query(Company).filter(Company.id == company_id).first()
        if not company:
            return self.MAIN_INCLUDES_BRANCHES
        addon = self.count_active_branch_licenses(company_id)
        if addon > 0:
            return self.MAIN_INCLUDES_BRANCHES + addon
        if company.license_key_id:
            record = (
                self.db.query(LicenseKey).filter(LicenseKey.id == company.license_key_id).first()
            )
            if record and record.max_branches:
                return int(record.max_branches)
        if settings.allow_legacy_unlicensed:
            return 500
        return self.MAIN_INCLUDES_BRANCHES

    def count_company_branches(self, company_id: str) -> int:
        from sqlalchemy import func

        return (
            self.db.query(func.count(Branch.id)).filter(Branch.company_id == company_id).scalar()
            or 0
        )

    def assert_can_add_branch(self, company_id: str) -> None:
        """Raise ValueError if the company is at its licensed branch limit."""
        if not settings.license_enforcement:
            return
        limit = self.get_max_branches_for_company(company_id)
        current = self.count_company_branches(company_id)
        if current >= limit:
            raise ValueError(
                f"Branch limit reached ({current}/{limit}). "
                "Contact CNP Solutions to upgrade your license for more branches."
            )

    def assert_branch_login_allowed(self, company_id: str, branch_id: Optional[str]) -> None:
        """Ensure company license is active and non-HQ branches have a branch license slot."""
        if not settings.license_enforcement:
            return
        lic = self.company_license_status(company_id)
        if not lic.get("licensed"):
            raise ValueError(lic.get("message", "Company license is not active"))
        if not branch_id:
            return
        branch = (
            self.db.query(Branch)
            .filter(Branch.id == branch_id, Branch.company_id == company_id)
            .first()
        )
        if not branch or branch.is_head_office:
            return
        uses_branch_slots = (
            self.db.query(BranchLicense)
            .filter(BranchLicense.company_id == company_id)
            .first()
            is not None
        )
        if not uses_branch_slots:
            return
        now = datetime.utcnow()
        slot = (
            self.db.query(BranchLicense)
            .join(LicenseKey, BranchLicense.parent_license_id == LicenseKey.id)
            .filter(
                BranchLicense.company_id == company_id,
                BranchLicense.branch_id == branch_id,
                BranchLicense.status == "active",
                BranchLicense.valid_until >= now,
                LicenseKey.status.in_(("pending", "active")),
                LicenseKey.valid_until >= now,
            )
            .first()
        )
        if not slot:
            raise ValueError(
                "This branch is not licensed or the license has expired. "
                "Contact CNP Solutions to renew or assign a branch license."
            )

    def assert_setup_branch_quota(
        self,
        raw_key: str,
        deployment: str,
        branches_to_create: int,
    ) -> None:
        """Ensure setup does not create more branches than the license allows (includes HQ)."""
        if not settings.license_required_for_setup:
            return
        if branches_to_create <= 0:
            return
        check = self.validate_for_setup(raw_key, deployment=deployment)
        if not check.get("valid"):
            raise ValueError(check.get("message", "Invalid license key"))
        max_allowed = int(check.get("max_branches") or self.MAIN_INCLUDES_BRANCHES)
        if max_allowed < 1:
            max_allowed = self.MAIN_INCLUDES_BRANCHES
        if branches_to_create > max_allowed:
            if max_allowed <= 1:
                raise ValueError(
                    f"Your license includes head office only (1 branch). "
                    f"Setup would create {branches_to_create} branches. "
                    "Remove extra branches from this step, finish setup with head office only, "
                    "then add branch licenses under God Mode → Customer licensing and create "
                    "additional branches in the ERP."
                )
            raise ValueError(
                f"Your license allows {max_allowed} branches including head office. "
                f"Setup would create {branches_to_create}. "
                "Remove extra branches here or ask CNP Solutions to raise the branch limit on "
                "your main license (God Mode → Issue main license)."
            )

    def company_license_status(self, company_id: str) -> Dict[str, Any]:
        company = self.db.query(Company).filter(Company.id == company_id).first()
        if not company:
            return {"licensed": False, "message": "Company not found"}

        if not settings.license_enforcement:
            return {"licensed": True, "enforcement": False, "message": "License enforcement disabled"}

        expires = company.license_expires_at
        record = None
        if company.license_key_id:
            record = self.db.query(LicenseKey).filter(LicenseKey.id == company.license_key_id).first()

        if not expires and record:
            expires = record.valid_until

        if not expires and not company.license_key_id:
            if settings.allow_legacy_unlicensed:
                return {
                    "licensed": True,
                    "status": "legacy",
                    "message": "Pre-licensing installation — assign a license key when ready.",
                }
            return {
                "licensed": False,
                "status": "unlicensed",
                "message": "No active license. Enter your CNP purchase token during setup or in Settings.",
            }

        now = datetime.utcnow()
        grace_days = (record.grace_days if record else 7) or 7
        grace_end = expires + timedelta(days=grace_days)

        if now <= expires:
            return {
                "licensed": True,
                "status": "active",
                "plan": record.plan if record else company.subscription_plan,
                "valid_until": expires.isoformat(),
                "days_remaining": (expires - now).days,
                "deployment_mode": company.deployment_mode,
                "key_prefix": record.key_prefix if record else None,
            }

        if now <= grace_end:
            return {
                "licensed": True,
                "status": "grace",
                "valid_until": expires.isoformat(),
                "grace_ends_at": grace_end.isoformat(),
                "days_remaining": 0,
                "message": f"License expired — grace period ends {grace_end.date().isoformat()}",
            }

        if record and record.status != "expired":
            record.status = "expired"
            self.db.flush()

        return {
            "licensed": False,
            "status": "expired",
            "valid_until": expires.isoformat(),
            "message": "License expired. Contact CNP Solutions to renew.",
        }

    def register_node(
        self,
        raw_key: str,
        node_name: str,
        public_base_url: Optional[str] = None,
        machine_fingerprint: Optional[str] = None,
        app_version: Optional[str] = None,
        client_ip: Optional[str] = None,
    ) -> Tuple[DeploymentNode, str]:
        """Register or refresh a local deployment node; returns (node, sync_token_plain)."""
        record = self.find_by_key(raw_key)
        if not record or record.status not in ("pending", "active"):
            raise ValueError("Invalid or inactive license for node registration")

        sync_plain = secrets.token_urlsafe(32)
        sync_hash = hashlib.sha256(sync_plain.encode()).hexdigest()

        existing = (
            self.db.query(DeploymentNode)
            .filter(
                DeploymentNode.license_key_id == record.id,
                DeploymentNode.machine_fingerprint == machine_fingerprint,
            )
            .first()
            if machine_fingerprint
            else None
        )

        now = datetime.utcnow()
        if existing:
            node = existing
            node.sync_token_hash = sync_hash
            node.last_heartbeat_at = now
            node.last_ip = client_ip
            node.app_version = app_version
            node.public_base_url = public_base_url or node.public_base_url
            node.is_active = True
        else:
            node = DeploymentNode(
                id=str(uuid.uuid4()),
                license_key_id=record.id,
                company_id=record.company_id,
                node_name=node_name,
                deployment_type="local",
                public_base_url=public_base_url,
                sync_token_hash=sync_hash,
                machine_fingerprint=machine_fingerprint,
                last_heartbeat_at=now,
                last_ip=client_ip,
                app_version=app_version,
            )
            self.db.add(node)

        self.db.flush()
        return node, sync_plain

    def heartbeat_node(
        self,
        node_id: str,
        sync_token: str,
        app_version: Optional[str] = None,
        client_ip: Optional[str] = None,
    ) -> DeploymentNode:
        token_hash = hashlib.sha256(sync_token.encode()).hexdigest()
        node = self.db.query(DeploymentNode).filter(DeploymentNode.id == node_id).first()
        if not node or node.sync_token_hash != token_hash or not node.is_active:
            raise ValueError("Invalid node or sync token")

        node.last_heartbeat_at = datetime.utcnow()
        node.last_ip = client_ip
        if app_version:
            node.app_version = app_version
        self.db.flush()
        return node
