"""GodotIQ licensing — PRO tool gating with Polar.sh key validation.

Community users can call PRO tools. The tool runs fully, then the result
is discarded and replaced with a teaser string showing real counts.

License sources (checked in order):
1. GODOTIQ_LICENSE=pro  — dev/test override only (bypasses validation)
2. GODOTIQ_LICENSE_KEY   — Polar.sh license key, validated via API
3. Disk cache at ~/.config/godotiq/license_cache.json (24h TTL)
4. Expired cache used as offline fallback when API is unreachable

Sandbox / testing env vars:
- GODOTIQ_POLAR_SANDBOX=true  — use Polar sandbox API endpoint
- GODOTIQ_POLAR_ORG_ID        — override the default organization ID
"""

from __future__ import annotations

import hashlib
import json
import logging
import os
import sys
import time
from pathlib import Path
from typing import Any

import httpx

logger = logging.getLogger(__name__)

# ── 13 PRO tools (intelligence & analysis) ────────────────────────────

PRO_TOOLS: frozenset[str] = frozenset({
    "godotiq_project_summary",
    "godotiq_file_context",
    "godotiq_dependency_graph",
    "godotiq_validate",
    "godotiq_signal_map",
    "godotiq_trace_flow",
    "godotiq_impact_check",
    "godotiq_animation_audit",
    "godotiq_asset_registry",
    "godotiq_scene_map",
    "godotiq_spatial_audit",
    "godotiq_suggest_scale",
    "godotiq_placement",
})

_UPGRADE_SUFFIX = " \u2192 upgrade: godotiq.com/pro"

# ── Polar.sh configuration ────────────────────────────────────────────

_POLAR_PROD_URL = "https://api.polar.sh/v1/customer-portal/license-keys/validate"
_POLAR_SANDBOX_URL = "https://sandbox-api.polar.sh/v1/customer-portal/license-keys/validate"
_POLAR_DEFAULT_ORG_ID = "a06a7d09-881c-430d-9775-e6a7529a0831"


def _polar_validate_url() -> str:
    """Return the Polar validation URL, using sandbox if GODOTIQ_POLAR_SANDBOX=true."""
    if os.environ.get("GODOTIQ_POLAR_SANDBOX", "").lower() == "true":
        return _POLAR_SANDBOX_URL
    return _POLAR_PROD_URL


def _polar_org_id() -> str:
    """Return the Polar org ID, allowing override via GODOTIQ_POLAR_ORG_ID."""
    return os.environ.get("GODOTIQ_POLAR_ORG_ID", "").strip() or _POLAR_DEFAULT_ORG_ID

_CACHE_TTL_SECONDS = 86400  # 24 hours


def _cache_dir() -> Path:
    """Return the platform-appropriate cache directory."""
    if sys.platform == "win32":
        appdata = os.environ.get("APPDATA")
        if appdata:
            return Path(appdata) / "godotiq"
    return Path.home() / ".config" / "godotiq"


_CACHE_DIR = _cache_dir()
_CACHE_FILE = _CACHE_DIR / "license_cache.json"


# ── In-process license cache ──────────────────────────────────────────
# Avoids repeated disk reads / API calls within a single server process.

_license_status: bool | None = None


def _reset_license_cache() -> None:
    """Reset the in-process license cache.  For testing only."""
    global _license_status
    _license_status = None


# ── License check ─────────────────────────────────────────────────────

def is_pro() -> bool:
    """Return True if the current environment has a valid PRO license.

    Check order:
    1. GODOTIQ_LICENSE=pro (dev/test override — not for production use)
    2. GODOTIQ_LICENSE_KEY validated against Polar.sh API
    3. Disk cache (24h TTL)
    4. Expired cache as offline fallback
    """
    global _license_status
    if _license_status is not None:
        return _license_status
    _license_status = _check_license()
    return _license_status


def _check_license() -> bool:
    """Run the full license validation chain."""
    # 1. Dev/test override
    if os.environ.get("GODOTIQ_LICENSE", "").lower() == "pro":
        return True

    # 2. License key from env
    key = os.environ.get("GODOTIQ_LICENSE_KEY", "").strip()
    if not key:
        return False

    key_hash = _hash_key(key)

    # 3. Check disk cache
    cache = _read_cache()
    if cache is not None and cache.get("key_hash") == key_hash:
        age = time.time() - cache.get("validated_at", 0)
        if age < _CACHE_TTL_SECONDS:
            return cache.get("valid", False)

    # 4. Validate against Polar API
    try:
        valid = _validate_polar(key)
        _write_cache(key_hash, valid)
        return valid
    except Exception:
        logger.debug("Polar API unreachable, falling back to cache")
        # 5. Offline fallback — use expired cache if key matches
        if cache is not None and cache.get("key_hash") == key_hash:
            return cache.get("valid", False)
        return False


# ── Polar API validation ──────────────────────────────────────────────

def _validate_polar(key: str) -> bool:
    """Validate a license key against the Polar.sh API.

    POST https://api.polar.sh/v1/customer-portal/license-keys/validate
    Body: {"key": key, "organization_id": org_id}
    No auth header needed.
    Valid if: HTTP 200 AND response.status == "granted".
    """
    url = _polar_validate_url()
    org = _polar_org_id()

    # Mask the key: show first 10 and last 4 chars
    masked = key[:10] + "..." + key[-4:] if len(key) > 14 else key

    resp = httpx.post(
        url,
        json={"key": key, "organization_id": org},
        timeout=10,
    )

    if resp.status_code != 200:
        logger.info("License: key=%s → denied (HTTP %s)", masked, resp.status_code)
        logger.debug("License detail: org=%s url=%s", org, url)
        return False

    data = resp.json()
    status = data.get("status", "unknown")
    result = "granted" if status == "granted" else f"denied ({status})"
    logger.info("License: key=%s → %s", masked, result)
    logger.debug("License detail: org=%s url=%s", org, url)
    return status == "granted"


# ── Disk cache ────────────────────────────────────────────────────────

def _hash_key(key: str) -> str:
    """SHA-256 hash of the license key (never store the key in plaintext)."""
    return hashlib.sha256(key.encode("utf-8")).hexdigest()


def _read_cache() -> dict | None:
    """Read license cache from disk.  Returns None on any error."""
    try:
        if not _CACHE_FILE.exists():
            return None
        data = json.loads(_CACHE_FILE.read_text(encoding="utf-8"))
        if not isinstance(data, dict):
            return None
        return data
    except Exception:
        return None


def _write_cache(key_hash: str, valid: bool) -> None:
    """Write license validation result to disk cache."""
    try:
        _CACHE_DIR.mkdir(parents=True, exist_ok=True)
        cache = {
            "key_hash": key_hash,
            "valid": valid,
            "validated_at": time.time(),
        }
        _CACHE_FILE.write_text(json.dumps(cache), encoding="utf-8")
    except Exception:
        logger.debug("Failed to write license cache", exc_info=True)


# ── Teaser builders ───────────────────────────────────────────────────
# Each takes the full tool result dict and returns a human-readable
# summary string.  Keys are accessed with .get() so missing data
# degrades gracefully.

def _teaser_project_summary(r: dict) -> str:
    counts = r.get("counts", {})
    scripts = counts.get("scripts", 0)
    scenes = counts.get("scenes", 0)
    autoloads = len(r.get("autoloads", []))
    return f"{scripts} scripts, {scenes} scenes, {autoloads} autoloads analyzed"


def _teaser_file_context(r: dict) -> str:
    api = r.get("public_api", {})
    funcs = len(api.get("functions", []))
    signals = len(r.get("signals_defined", []))
    depended = len(r.get("depended_by", []))
    return f"{funcs} public functions, {signals} signals, depended by {depended} scripts"


def _teaser_dependency_graph(r: dict) -> str:
    emits = len(r.get("emits_signals", []))
    refs = len(r.get("referenced_by", []))
    impact = r.get("impact_rating", "unknown")
    return f"{emits} signals emitted, {refs} reverse refs, impact: {impact}"


def _teaser_validate(r: dict) -> str:
    total = r.get("total_issues", 0)
    by_sev = r.get("by_severity", {})
    errors = by_sev.get("error", 0)
    warnings = by_sev.get("warning", 0)
    info = by_sev.get("info", 0)
    return f"{total} issues: {errors} errors, {warnings} warnings, {info} info"


def _teaser_signal_map(r: dict) -> str:
    defined = r.get("total_signals_defined", 0)
    connections = r.get("total_connections", 0)
    orphans = len(r.get("orphans", []))
    return f"{defined} signals, {connections} connections, {orphans} orphans"


def _teaser_trace_flow(r: dict) -> str:
    steps = r.get("total_steps", 0)
    files = r.get("files_involved", 0)
    failures = len(r.get("failure_points", []))
    return f"{steps} steps across {files} files, {failures} failure points"


def _teaser_impact_check(r: dict) -> str:
    risk = r.get("risk", "unknown")
    affected = r.get("total_files_affected", 0)
    callers = len(r.get("callers", []))
    return f"risk: {risk}, {affected} files affected, {callers} callers found"


def _teaser_animation_audit(r: dict) -> str:
    anims = r.get("total_animations", 0)
    total = r.get("total_issues", 0)
    by_sev = r.get("by_severity", {})
    critical = by_sev.get("critical", 0)
    warnings = by_sev.get("warning", 0)
    return f"{anims} animations scanned, {total} issues: {critical} critical, {warnings} warnings"


def _teaser_asset_registry(r: dict) -> str:
    total = r.get("total_assets", 0)
    unused = r.get("unused_count", 0)
    categories = len(r.get("by_category", {}))
    return f"{total} assets cataloged, {unused} unused ({categories} categories)"


def _teaser_scene_map(r: dict) -> str:
    nodes = r.get("total_nodes", 0)
    summary = r.get("spatial_summary", {})
    bounds = summary.get("bounds", {})
    bmin = bounds.get("min", [0, 0, 0])
    bmax = bounds.get("max", [0, 0, 0])
    w = abs(bmax[0] - bmin[0]) if len(bmin) >= 3 and len(bmax) >= 3 else 0
    h = abs(bmax[1] - bmin[1]) if len(bmin) >= 3 and len(bmax) >= 3 else 0
    d = abs(bmax[2] - bmin[2]) if len(bmin) >= 3 and len(bmax) >= 3 else 0
    return f"{nodes} nodes mapped, bounds: {w:.0f}x{h:.0f}x{d:.0f}m"


def _teaser_spatial_audit(r: dict) -> str:
    total = r.get("total_issues", 0)
    by_sev = r.get("by_severity", {})
    warnings = by_sev.get("warning", 0)
    info = by_sev.get("info", 0)
    checks = len(r.get("checks_run", []))
    return f"{total} issues: {warnings} warnings, {info} info across {checks} checks"


def _teaser_suggest_scale(r: dict) -> str:
    refs = len(r.get("reference_models", r.get("references", [])))
    tier = r.get("match_tier", "unknown")
    return f"{refs} reference models analyzed, match: {tier}"


def _teaser_placement(r: dict) -> str:
    evaluated = r.get("candidates_evaluated", 0)
    suggestions = len(r.get("suggestions", []))
    zones = len(r.get("excluded_zones", []))
    return f"{evaluated} positions evaluated, {suggestions} valid, {zones} excluded zones"


_TEASER_BUILDERS: dict[str, Any] = {
    "godotiq_project_summary": _teaser_project_summary,
    "godotiq_file_context": _teaser_file_context,
    "godotiq_dependency_graph": _teaser_dependency_graph,
    "godotiq_validate": _teaser_validate,
    "godotiq_signal_map": _teaser_signal_map,
    "godotiq_trace_flow": _teaser_trace_flow,
    "godotiq_impact_check": _teaser_impact_check,
    "godotiq_animation_audit": _teaser_animation_audit,
    "godotiq_asset_registry": _teaser_asset_registry,
    "godotiq_scene_map": _teaser_scene_map,
    "godotiq_spatial_audit": _teaser_spatial_audit,
    "godotiq_suggest_scale": _teaser_suggest_scale,
    "godotiq_placement": _teaser_placement,
}


# ── Gate function ─────────────────────────────────────────────────────

def gate_pro(tool_name: str, result: dict) -> dict:
    """Gate a PRO tool result.  Returns result as-is for PRO users.

    For Community users, discards the full result and returns a teaser
    dict with real counts extracted from the analysis.

    Error results (containing "error" key) always pass through ungated
    so Community users still see actionable error messages.
    """
    # Not a PRO tool — pass through
    if tool_name not in PRO_TOOLS:
        return result

    # PRO license — pass through
    if is_pro():
        return result

    # Error results — pass through (community users need error messages)
    if "error" in result:
        return result

    # Community user calling PRO tool — build teaser
    builder = _TEASER_BUILDERS.get(tool_name)
    if builder is None:
        teaser = f"Analysis complete{_UPGRADE_SUFFIX}"
    else:
        teaser = builder(result) + _UPGRADE_SUFFIX

    return {
        "tier": "community",
        "tool": tool_name,
        "teaser": teaser,
    }
