"""Signed invite-code licensing with append-only hash-chain activation ledger."""

from __future__ import annotations

import base64
import hashlib
import json
import os
import platform
import uuid
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional

from model_osmosis.format.trust import (
    sign_statement,
    trust_home,
    verify_trust_block,
)


INVITE_CODE_SCHEMA_VERSION = "1"
INVITE_CODE_PREFIX = "osmoinv1_"
LEDGER_SCHEMA_VERSION = "1"
LICENSE_SCHEMA_VERSION = "1"
GENESIS_PREV_HASH = "GENESIS"
ISSUER_LOCK_SCHEMA_VERSION = "1"
ISSUER_MODE_ENV = "OSMOSIS_INVITE_ISSUER"


def _utc_now() -> str:
    return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


def _parse_utc(value: str) -> datetime:
    return datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)


def _sha256_bytes(data: bytes) -> str:
    return hashlib.sha256(data).hexdigest()


def _sha256_text(text: str) -> str:
    return _sha256_bytes(text.encode("utf-8"))


def _canonical_json(data: Dict[str, Any]) -> str:
    return json.dumps(data, sort_keys=True, separators=(",", ":"))


def _activation_root() -> Path:
    path = trust_home() / "invite_licensing"
    path.mkdir(parents=True, exist_ok=True)
    return path


def _ledger_path() -> Path:
    return _activation_root() / "activation_ledger.jsonl"


def _license_path() -> Path:
    return _activation_root() / "active_license.json"


def _issuer_lock_path() -> Path:
    return _activation_root() / "issuer_authority.json"


def _default_machine_id() -> str:
    # Stable per-machine fingerprint for local invite activation state.
    raw = "|".join(
        [
            platform.node() or "unknown-host",
            platform.system() or "unknown-os",
            platform.machine() or "unknown-arch",
            str(uuid.getnode()),
        ]
    )
    return _sha256_text(raw)[:24]


def _issuer_mode_enabled() -> bool:
    return str(os.environ.get(ISSUER_MODE_ENV, "0")).strip().lower() in {"1", "true", "yes", "on"}


def _load_issuer_lock() -> Dict[str, Any]:
    path = _issuer_lock_path()
    if not path.exists():
        return {}
    try:
        payload = json.loads(path.read_text(encoding="utf-8"))
    except Exception as exc:
        raise RuntimeError(f"Issuer lock file is invalid JSON: {path}") from exc
    if not isinstance(payload, dict):
        raise RuntimeError("Issuer lock file must contain a JSON object")
    return payload


def _save_issuer_lock(payload: Dict[str, Any]) -> None:
    path = _issuer_lock_path()
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")


def _ensure_issuer_authority(signer_id: str) -> Dict[str, Any]:
    if not _issuer_mode_enabled():
        raise PermissionError(
            "Invite code generation is restricted to issuer mode. "
            f"Set {ISSUER_MODE_ENV}=1 on the authorized issuer machine."
        )

    current_machine = _default_machine_id()
    lock = _load_issuer_lock()
    if not lock:
        lock = {
            "schema_version": ISSUER_LOCK_SCHEMA_VERSION,
            "created_at": _utc_now(),
            "machine_id": current_machine,
            "signer_id": str(signer_id),
        }
        _save_issuer_lock(lock)
        return lock

    locked_machine = str(lock.get("machine_id", "") or "")
    if locked_machine != current_machine:
        raise PermissionError(
            "Issuer authority is locked to a different machine. "
            "Invite creation is allowed only on the original issuer machine."
        )
    locked_signer = str(lock.get("signer_id", "") or "")
    if locked_signer and str(signer_id) != locked_signer:
        raise PermissionError(
            f"Issuer signer mismatch: expected '{locked_signer}', got '{signer_id}'."
        )
    return lock


def _encode_invite_blob(blob: Dict[str, Any]) -> str:
    payload = _canonical_json(blob).encode("utf-8")
    token = base64.urlsafe_b64encode(payload).decode("ascii").rstrip("=")
    return f"{INVITE_CODE_PREFIX}{token}"


def _decode_invite_blob(code: str) -> Dict[str, Any]:
    raw = str(code or "").strip()
    if not raw:
        raise ValueError("Invite code is empty")
    token = raw
    if token.startswith(INVITE_CODE_PREFIX):
        token = token[len(INVITE_CODE_PREFIX):]
    padded = token + "=" * ((4 - (len(token) % 4)) % 4)
    try:
        decoded = base64.urlsafe_b64decode(padded.encode("ascii"))
        blob = json.loads(decoded.decode("utf-8"))
    except Exception as exc:
        raise ValueError("Invite code is malformed") from exc
    if not isinstance(blob, dict):
        raise ValueError("Invite code payload must be a JSON object")
    return blob


def _invite_claims_from_blob(blob: Dict[str, Any]) -> Dict[str, Any]:
    trust_block = blob.get("trust")
    if not isinstance(trust_block, dict):
        raise ValueError("Invite code missing trust block")
    statement = trust_block.get("statement")
    if not isinstance(statement, dict):
        raise ValueError("Invite trust statement is missing")
    claims = statement.get("claims")
    if not isinstance(claims, dict):
        raise ValueError("Invite claims are missing")
    return claims


def _event_hash(event: Dict[str, Any]) -> str:
    body = {k: v for k, v in event.items() if k != "event_hash"}
    return _sha256_text(_canonical_json(body))


def load_activation_ledger() -> Dict[str, Any]:
    path = _ledger_path()
    if not path.exists():
        return {
            "valid": True,
            "reason": "",
            "events": [],
            "head_hash": GENESIS_PREV_HASH,
            "event_count": 0,
            "path": str(path),
        }

    events: List[Dict[str, Any]] = []
    prev_hash = GENESIS_PREV_HASH
    with path.open("r", encoding="utf-8") as fh:
        for line_no, raw_line in enumerate(fh, start=1):
            line = raw_line.strip()
            if not line:
                continue
            try:
                event = json.loads(line)
            except Exception as exc:
                return {
                    "valid": False,
                    "reason": f"ledger-json-error-line-{line_no}: {exc}",
                    "events": events,
                    "head_hash": prev_hash,
                    "event_count": len(events),
                    "path": str(path),
                }
            if not isinstance(event, dict):
                return {
                    "valid": False,
                    "reason": f"ledger-event-not-object-line-{line_no}",
                    "events": events,
                    "head_hash": prev_hash,
                    "event_count": len(events),
                    "path": str(path),
                }
            if str(event.get("prev_hash", "")) != prev_hash:
                return {
                    "valid": False,
                    "reason": f"ledger-prev-hash-mismatch-line-{line_no}",
                    "events": events,
                    "head_hash": prev_hash,
                    "event_count": len(events),
                    "path": str(path),
                }
            expected = _event_hash(event)
            actual = str(event.get("event_hash", ""))
            if actual != expected:
                return {
                    "valid": False,
                    "reason": f"ledger-event-hash-mismatch-line-{line_no}",
                    "events": events,
                    "head_hash": prev_hash,
                    "event_count": len(events),
                    "path": str(path),
                }
            prev_hash = actual
            events.append(event)

    return {
        "valid": True,
        "reason": "",
        "events": events,
        "head_hash": prev_hash,
        "event_count": len(events),
        "path": str(path),
    }


def _append_ledger_event(event: Dict[str, Any]) -> Dict[str, Any]:
    ledger = load_activation_ledger()
    if not ledger.get("valid", False):
        raise RuntimeError(f"Activation ledger is invalid: {ledger.get('reason')}")

    payload = dict(event)
    payload["schema_version"] = LEDGER_SCHEMA_VERSION
    payload["prev_hash"] = str(ledger.get("head_hash", GENESIS_PREV_HASH))
    payload["event_hash"] = _event_hash(payload)

    path = _ledger_path()
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("a", encoding="utf-8") as fh:
        fh.write(_canonical_json(payload) + "\n")
    return payload


def _activation_count(events: List[Dict[str, Any]], invite_id: str) -> int:
    return sum(
        1
        for event in events
        if str(event.get("event_type", "")) == "activation"
        and str(event.get("invite_id", "")) == invite_id
    )


def _is_machine_activated(events: List[Dict[str, Any]], invite_id: str, machine_id: str) -> bool:
    return any(
        str(event.get("event_type", "")) == "activation"
        and str(event.get("invite_id", "")) == invite_id
        and str(event.get("machine_id", "")) == machine_id
        for event in events
    )


def create_invite_code(
    *,
    signer_id: str = "default",
    subject: Optional[str] = None,
    max_activations: int = 1,
    valid_days: Optional[int] = 30,
    expires_at: Optional[str] = None,
    metadata: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
    _ensure_issuer_authority(str(signer_id))

    if int(max_activations) <= 0:
        raise ValueError("max_activations must be >= 1")
    if valid_days is not None and int(valid_days) <= 0:
        raise ValueError("valid_days must be >= 1 when provided")

    issued_at = _utc_now()
    if expires_at:
        parsed_expires = _parse_utc(str(expires_at))
    else:
        days = int(valid_days if valid_days is not None else 30)
        parsed_expires = _parse_utc(issued_at) + timedelta(days=days)
    expires_str = parsed_expires.strftime("%Y-%m-%dT%H:%M:%SZ")

    nonce = _sha256_bytes(os.urandom(16))[:20]
    invite_id = f"inv_{nonce}"
    claims: Dict[str, Any] = {
        "schema_version": INVITE_CODE_SCHEMA_VERSION,
        "invite_id": invite_id,
        "issued_at": issued_at,
        "expires_at": expires_str,
        "max_activations": int(max_activations),
        "subject": str(subject or ""),
    }
    if isinstance(metadata, dict) and metadata:
        claims["metadata"] = json.loads(_canonical_json(metadata))

    claims_bytes = _canonical_json(claims).encode("utf-8")
    statement = {
        "artifact_version": "invite-code-v1",
        "payload_sha256": _sha256_bytes(claims_bytes),
        "claims": claims,
        "tool": "model-osmosis",
    }
    trust_block = sign_statement(statement, signer_id=signer_id, ensure_signer=True)
    payload = {
        "schema_version": INVITE_CODE_SCHEMA_VERSION,
        "trust": trust_block,
    }
    code = _encode_invite_blob(payload)
    return {
        "invite_code": code,
        "invite_id": invite_id,
        "signer_id": trust_block.get("signer_id"),
        "key_id": trust_block.get("key_id"),
        "issued_at": issued_at,
        "expires_at": expires_str,
        "max_activations": int(max_activations),
        "subject": str(subject or ""),
    }


def verify_invite_code(code: str) -> Dict[str, Any]:
    try:
        blob = _decode_invite_blob(code)
        trust_block = blob.get("trust", {})
        claims = _invite_claims_from_blob(blob)
    except Exception as exc:
        return {
            "status": "invalid",
            "trust_status": "tampered",
            "reason": str(exc),
            "invite_id": None,
            "signer_id": None,
            "key_id": None,
            "expires_at": None,
            "max_activations": None,
            "activations_used": 0,
            "remaining_activations": 0,
            "is_expired": False,
        }

    claims_bytes = _canonical_json(claims).encode("utf-8")
    trust_result = verify_trust_block(trust_block, payload_bytes=claims_bytes, require_trust=False)
    trust_status = str(trust_result.get("trust_status", "unsigned"))
    invite_id = str(claims.get("invite_id", "") or "")
    max_activations = int(claims.get("max_activations", 0) or 0)
    expires_at = str(claims.get("expires_at", "") or "")
    is_expired = False
    if expires_at:
        try:
            is_expired = _parse_utc(expires_at) < datetime.now(timezone.utc)
        except Exception:
            trust_status = "tampered"
            trust_result["reason"] = "Invalid invite expiration timestamp"

    ledger = load_activation_ledger()
    events = list(ledger.get("events", []))
    activations_used = _activation_count(events, invite_id) if invite_id else 0
    remaining = max(max_activations - activations_used, 0) if max_activations > 0 else 0

    status = "valid"
    reason = str(trust_result.get("reason", ""))
    if trust_status in {"unsigned", "tampered"}:
        status = "invalid"
    elif is_expired:
        status = "expired"
        reason = "Invite code expired"

    return {
        "status": status,
        "trust_status": trust_status,
        "reason": reason,
        "invite_id": invite_id or None,
        "signer_id": trust_result.get("signer_id"),
        "key_id": trust_result.get("key_id"),
        "expires_at": expires_at or None,
        "max_activations": max_activations,
        "activations_used": activations_used,
        "remaining_activations": remaining,
        "is_expired": is_expired,
        "ledger_valid": bool(ledger.get("valid", False)),
        "ledger_reason": str(ledger.get("reason", "")),
        "ledger_head_hash": str(ledger.get("head_hash", GENESIS_PREV_HASH)),
    }


def activate_invite_code(
    code: str,
    *,
    machine_id: Optional[str] = None,
    require_trusted_signer: bool = False,
) -> Dict[str, Any]:
    machine = str(machine_id or _default_machine_id()).strip()
    if not machine:
        raise ValueError("machine_id cannot be empty")

    verify = verify_invite_code(code)
    status = str(verify.get("status", "invalid"))
    if status == "expired":
        raise RuntimeError("Invite code expired")
    if status != "valid":
        raise RuntimeError(f"Invite code invalid: {verify.get('reason', 'unknown error')}")
    if require_trusted_signer and verify.get("trust_status") != "trusted":
        raise RuntimeError("Invite signer is not trusted locally")

    invite_id = str(verify.get("invite_id") or "")
    if not invite_id:
        raise RuntimeError("Invite code missing invite_id")
    max_activations = int(verify.get("max_activations", 0) or 0)

    ledger = load_activation_ledger()
    if not ledger.get("valid", False):
        raise RuntimeError(f"Activation ledger is invalid: {ledger.get('reason')}")
    events = list(ledger.get("events", []))
    activations_used = _activation_count(events, invite_id)

    if _is_machine_activated(events, invite_id, machine):
        return {
            "status": "already_active",
            "invite_id": invite_id,
            "machine_id": machine,
            "activations_used": activations_used,
            "max_activations": max_activations,
            "remaining_activations": max(max_activations - activations_used, 0),
            "trust_status": verify.get("trust_status"),
            "signer_id": verify.get("signer_id"),
            "key_id": verify.get("key_id"),
            "ledger_head_hash": ledger.get("head_hash", GENESIS_PREV_HASH),
        }

    if activations_used >= max_activations:
        raise RuntimeError(
            f"Invite activation limit reached ({activations_used}/{max_activations})"
        )

    event = _append_ledger_event(
        {
            "event_type": "activation",
            "created_at": _utc_now(),
            "invite_id": invite_id,
            "machine_id": machine,
            "signer_id": verify.get("signer_id"),
            "key_id": verify.get("key_id"),
            "trust_status": verify.get("trust_status"),
            "code_fingerprint": _sha256_text(str(code))[:16],
        }
    )

    license_payload = {
        "schema_version": LICENSE_SCHEMA_VERSION,
        "activated_at": str(event.get("created_at")),
        "invite_id": invite_id,
        "machine_id": machine,
        "signer_id": verify.get("signer_id"),
        "key_id": verify.get("key_id"),
        "trust_status": verify.get("trust_status"),
        "expires_at": verify.get("expires_at"),
        "max_activations": max_activations,
        "invite_code": str(code),
        "ledger_event_hash": event.get("event_hash"),
    }
    path = _license_path()
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(license_payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")

    return {
        "status": "activated",
        "invite_id": invite_id,
        "machine_id": machine,
        "activations_used": activations_used + 1,
        "max_activations": max_activations,
        "remaining_activations": max(max_activations - (activations_used + 1), 0),
        "trust_status": verify.get("trust_status"),
        "signer_id": verify.get("signer_id"),
        "key_id": verify.get("key_id"),
        "ledger_event_hash": event.get("event_hash"),
        "ledger_head_hash": event.get("event_hash"),
        "license_file": str(path),
    }


def license_status() -> Dict[str, Any]:
    path = _license_path()
    ledger = load_activation_ledger()
    if not path.exists():
        return {
            "status": "inactive",
            "reason": "No active license file",
            "license_file": str(path),
            "ledger_valid": bool(ledger.get("valid", False)),
            "ledger_reason": str(ledger.get("reason", "")),
        }

    try:
        data = json.loads(path.read_text(encoding="utf-8"))
    except Exception as exc:
        return {
            "status": "invalid",
            "reason": f"License file is invalid JSON: {exc}",
            "license_file": str(path),
            "ledger_valid": bool(ledger.get("valid", False)),
            "ledger_reason": str(ledger.get("reason", "")),
        }

    if not isinstance(data, dict):
        return {
            "status": "invalid",
            "reason": "License file must contain a JSON object",
            "license_file": str(path),
            "ledger_valid": bool(ledger.get("valid", False)),
            "ledger_reason": str(ledger.get("reason", "")),
        }

    code = str(data.get("invite_code", "") or "")
    if not code:
        return {
            "status": "invalid",
            "reason": "License file missing invite_code",
            "license_file": str(path),
            "ledger_valid": bool(ledger.get("valid", False)),
            "ledger_reason": str(ledger.get("reason", "")),
        }

    verify = verify_invite_code(code)
    machine_id = str(data.get("machine_id", "") or "")
    if machine_id and machine_id != _default_machine_id():
        return {
            "status": "invalid",
            "reason": "License is bound to a different machine",
            "license_file": str(path),
            "machine_id": machine_id,
            "current_machine_id": _default_machine_id(),
            "verify": verify,
            "ledger_valid": bool(ledger.get("valid", False)),
            "ledger_reason": str(ledger.get("reason", "")),
        }
    if verify.get("status") == "expired":
        return {
            "status": "expired",
            "reason": "Invite license expired",
            "license_file": str(path),
            "machine_id": machine_id or _default_machine_id(),
            "verify": verify,
            "ledger_valid": bool(ledger.get("valid", False)),
            "ledger_reason": str(ledger.get("reason", "")),
        }
    if verify.get("status") != "valid":
        return {
            "status": "invalid",
            "reason": str(verify.get("reason") or "Invite verification failed"),
            "license_file": str(path),
            "machine_id": machine_id or _default_machine_id(),
            "verify": verify,
            "ledger_valid": bool(ledger.get("valid", False)),
            "ledger_reason": str(ledger.get("reason", "")),
        }
    return {
        "status": "active",
        "reason": "License is active",
        "license_file": str(path),
        "machine_id": machine_id or _default_machine_id(),
        "verify": verify,
        "ledger_valid": bool(ledger.get("valid", False)),
        "ledger_reason": str(ledger.get("reason", "")),
    }
