"""Agentic authentication for Horizon SDK.

Allows agents (MCP, OpenClaw) to create a Horizon account and generate
an SDK key programmatically via the Supabase REST API, without requiring
the user to visit the website.

Uses only stdlib (urllib, json, hashlib, secrets) to avoid adding dependencies.

Security notes:
- Passwords should be passed via HORIZON_PASSWORD env var, not CLI args.
- The full API key is never returned in tool responses (only the prefix).
- Credentials are encrypted at rest using a machine-derived key.
- Client-side rate limiting prevents signup/login abuse.
"""

from __future__ import annotations

import base64
import hashlib
import json
import os
import secrets
import time
from datetime import datetime, timezone
from pathlib import Path
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen

# Public constants (already embedded in src/auth.rs)
SUPABASE_URL = "https://pjhydvnnptabkiiluoji.supabase.co"
SUPABASE_ANON_KEY = (
    "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
    "eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBqaHlkdm5ucHRhYmtpaWx1b2ppIiwi"
    "cm9sZSI6ImFub24iLCJpYXQiOjE3NzA3NTMwMDQsImV4cCI6MjA4NjMyOTAwNH0."
    "vwa7fwDIwDX9rJkWDq9K_mgWyCMsGqnKE3OMNOR8KOM"
)

_CREDENTIALS_DIR = Path.home() / ".horizon"
_CREDENTIALS_FILE = _CREDENTIALS_DIR / "credentials.json"
_RATE_LIMIT_FILE = _CREDENTIALS_DIR / ".auth_rate_limit"
_TIMEOUT = 10

# Rate limiting: max 5 auth attempts per 60 seconds
_RATE_LIMIT_MAX = 5
_RATE_LIMIT_WINDOW = 60


# ---------------------------------------------------------------------------
# Key generation (matches platform algorithm)
# ---------------------------------------------------------------------------

def generate_sdk_key() -> str:
    """Generate an SDK key: ``hz_sdk_`` + 64 random hex chars."""
    return "hz_sdk_" + secrets.token_hex(32)


def hash_api_key(key: str) -> str:
    """SHA-256 hash of an API key (same algorithm as the deploy platform)."""
    return hashlib.sha256(key.encode()).hexdigest()


# ---------------------------------------------------------------------------
# Credential encryption at rest
# ---------------------------------------------------------------------------

def _derive_machine_key() -> bytes:
    """Derive a 32-byte encryption key from machine-specific data.

    Uses hostname + username + a fixed salt. This is not a substitute for
    a proper key vault, but it ensures the credentials file is not trivially
    readable as plaintext if copied to another machine or user account.
    """
    import getpass
    import platform
    material = f"horizon::{platform.node()}::{getpass.getuser()}::sdk_cred_v1"
    return hashlib.sha256(material.encode()).digest()


def _xor_bytes(data: bytes, key: bytes) -> bytes:
    """XOR data with a repeating key."""
    key_len = len(key)
    return bytes(b ^ key[i % key_len] for i, b in enumerate(data))


def _encrypt_key(api_key: str) -> str:
    """Encrypt an API key for storage. Returns base64-encoded ciphertext."""
    machine_key = _derive_machine_key()
    plaintext = api_key.encode()
    ciphertext = _xor_bytes(plaintext, machine_key)
    return base64.b64encode(ciphertext).decode()


def _decrypt_key(encrypted: str) -> str:
    """Decrypt an API key from storage."""
    machine_key = _derive_machine_key()
    ciphertext = base64.b64decode(encrypted)
    plaintext = _xor_bytes(ciphertext, machine_key)
    return plaintext.decode()


# ---------------------------------------------------------------------------
# Client-side rate limiting
# ---------------------------------------------------------------------------

def _check_rate_limit() -> str | None:
    """Check if auth rate limit has been exceeded.

    Returns an error message string if exceeded, None if OK.
    """
    try:
        _CREDENTIALS_DIR.mkdir(parents=True, exist_ok=True)
        now = time.time()
        attempts: list[float] = []

        if _RATE_LIMIT_FILE.exists():
            try:
                data = json.loads(_RATE_LIMIT_FILE.read_text())
                attempts = [t for t in data.get("attempts", []) if now - t < _RATE_LIMIT_WINDOW]
            except Exception:
                attempts = []

        if len(attempts) >= _RATE_LIMIT_MAX:
            wait = int(_RATE_LIMIT_WINDOW - (now - attempts[0])) + 1
            return f"rate limit exceeded: max {_RATE_LIMIT_MAX} auth attempts per {_RATE_LIMIT_WINDOW}s. Try again in {wait}s."

        attempts.append(now)
        _RATE_LIMIT_FILE.write_text(json.dumps({"attempts": attempts}))
        try:
            _RATE_LIMIT_FILE.chmod(0o600)
        except OSError:
            pass
        return None
    except Exception:
        # If rate limiting fails, allow the request
        return None


# ---------------------------------------------------------------------------
# Password resolution
# ---------------------------------------------------------------------------

def resolve_password(password: str | None = None) -> str | None:
    """Resolve password from argument or HORIZON_PASSWORD env var.

    Prefers the env var to avoid password exposure in CLI args and logs.
    Returns None if no password is available.
    """
    env_pw = os.environ.get("HORIZON_PASSWORD", "")
    if env_pw:
        return env_pw
    if password:
        return password
    return None


# ---------------------------------------------------------------------------
# Supabase Auth helpers
# ---------------------------------------------------------------------------

def _supabase_request(path: str, body: dict, access_token: str | None = None) -> dict:
    """Make a POST request to the Supabase REST/Auth API."""
    url = f"{SUPABASE_URL}{path}"
    data = json.dumps(body).encode()
    headers = {
        "apikey": SUPABASE_ANON_KEY,
        "Content-Type": "application/json",
    }
    if access_token:
        headers["Authorization"] = f"Bearer {access_token}"
    else:
        headers["Authorization"] = f"Bearer {SUPABASE_ANON_KEY}"

    req = Request(url, data=data, headers=headers, method="POST")
    try:
        with urlopen(req, timeout=_TIMEOUT) as resp:
            return json.loads(resp.read().decode())
    except HTTPError as e:
        try:
            err_body = json.loads(e.read().decode())
        except Exception:
            err_body = {"message": str(e)}
        return {"error": err_body.get("msg") or err_body.get("message") or err_body.get("error_description") or str(e)}
    except URLError as e:
        return {"error": f"network error: {e.reason}"}
    except Exception as e:
        return {"error": str(e)}


def signup(email: str, password: str, name: str = "") -> dict:
    """Create a Horizon account via Supabase Auth.

    Returns ``{"status": "ok", "access_token": ..., ...}`` on success.
    If ``session`` is ``None``, email confirmation is required.
    Returns ``{"error": "..."}`` on failure.
    """
    rate_err = _check_rate_limit()
    if rate_err:
        return {"error": rate_err}

    body: dict = {"email": email, "password": password}
    if name:
        body["data"] = {"name": name}
    result = _supabase_request("/auth/v1/signup", body)
    if "error" in result:
        return result
    session = result.get("session")
    if session is None:
        return {
            "status": "confirmation_required",
            "message": "Check your email to confirm your account before generating an SDK key.",
            "email": email,
        }
    return {
        "status": "ok",
        "access_token": result["session"]["access_token"],
        "user_id": result["user"]["id"],
        "email": email,
    }


def login(email: str, password: str) -> dict:
    """Log into an existing Horizon account.

    Returns ``{"status": "ok", "access_token": ..., ...}`` on success.
    Returns ``{"error": "..."}`` on failure.
    """
    rate_err = _check_rate_limit()
    if rate_err:
        return {"error": rate_err}

    result = _supabase_request(
        "/auth/v1/token?grant_type=password",
        {"email": email, "password": password},
    )
    if "error" in result:
        return result
    return {
        "status": "ok",
        "access_token": result["access_token"],
        "user_id": result["user"]["id"],
        "email": email,
    }


# ---------------------------------------------------------------------------
# SDK key creation via Supabase REST
# ---------------------------------------------------------------------------

def _decode_jwt_payload(token: str) -> dict:
    """Decode a JWT payload for extracting the user ID (``sub`` claim).

    WARNING: This does NOT verify the JWT signature. It is used only to
    extract the ``sub`` claim for the SDK key insert. The actual
    authentication is performed by the Supabase server via the Bearer
    token. Do not use this function for security decisions.
    """
    if not isinstance(token, str) or not token:
        return {}
    parts = token.split(".")
    if len(parts) != 3:
        return {}
    payload = parts[1]
    # Reject obviously malformed payloads (too short or too long)
    if len(payload) < 4 or len(payload) > 4096:
        return {}
    # Pad to multiple of 4
    payload += "=" * (-len(payload) % 4)
    try:
        decoded = base64.urlsafe_b64decode(payload)
        result = json.loads(decoded)
        if not isinstance(result, dict):
            return {}
        return result
    except Exception:
        return {}


def create_sdk_key(access_token: str, name: str = "default") -> dict:
    """Generate an SDK key and insert it into the Supabase ``sdk_keys`` table.

    Returns ``{"key": "hz_sdk_...", "prefix": "hz_sdk_abcdef1"}`` on success.
    The full key is returned here but should NOT be forwarded to tool responses.
    """
    claims = _decode_jwt_payload(access_token)
    user_id = claims.get("sub")
    if not user_id:
        return {"error": "could not extract user ID from access token"}

    key = generate_sdk_key()
    key_hash = hash_api_key(key)
    prefix = key[:15]

    body = {
        "user_id": user_id,
        "key_hash": key_hash,
        "prefix": prefix,
        "name": name,
    }

    url = f"{SUPABASE_URL}/rest/v1/sdk_keys"
    data = json.dumps(body).encode()
    headers = {
        "apikey": SUPABASE_ANON_KEY,
        "Authorization": f"Bearer {access_token}",
        "Content-Type": "application/json",
        "Prefer": "return=minimal",
    }
    req = Request(url, data=data, headers=headers, method="POST")
    try:
        with urlopen(req, timeout=_TIMEOUT) as resp:
            # 201 Created with return=minimal gives empty body
            pass
    except HTTPError as e:
        try:
            err_body = json.loads(e.read().decode())
        except Exception:
            err_body = {}
        return {"error": err_body.get("message") or err_body.get("msg") or str(e)}
    except URLError as e:
        return {"error": f"network error: {e.reason}"}
    except Exception as e:
        return {"error": str(e)}

    return {"key": key, "prefix": prefix}


# ---------------------------------------------------------------------------
# Local credential storage (encrypted at rest)
# ---------------------------------------------------------------------------

def save_credentials(api_key: str) -> str:
    """Save an SDK key to ``~/.horizon/credentials.json`` (encrypted).

    Returns the path where the credentials were saved.
    """
    _CREDENTIALS_DIR.mkdir(parents=True, exist_ok=True)
    try:
        _CREDENTIALS_DIR.chmod(0o700)
    except OSError:
        pass

    payload = {
        "api_key_encrypted": _encrypt_key(api_key),
        "prefix": api_key[:15],
        "created_at": datetime.now(timezone.utc).isoformat(),
        "version": 2,
    }
    _CREDENTIALS_FILE.write_text(json.dumps(payload, indent=2))
    try:
        _CREDENTIALS_FILE.chmod(0o600)
    except OSError:
        pass
    return str(_CREDENTIALS_FILE)


def load_credentials() -> str | None:
    """Load the saved SDK key from ``~/.horizon/credentials.json``.

    Supports both v2 (encrypted) and v1 (plaintext, legacy) formats.
    Returns the key string, or ``None`` if not found or decryption fails.
    """
    if not _CREDENTIALS_FILE.exists():
        return None
    try:
        data = json.loads(_CREDENTIALS_FILE.read_text())
        version = data.get("version", 1)
        if version >= 2:
            encrypted = data.get("api_key_encrypted")
            if not encrypted:
                return None
            return _decrypt_key(encrypted)
        # v1 legacy: plaintext
        return data.get("api_key")
    except Exception:
        return None


# ---------------------------------------------------------------------------
# Full setup flow
# ---------------------------------------------------------------------------

def setup(email: str, password: str, name: str = "") -> dict:
    """Full agentic auth flow: signup/login, generate key, save locally.

    1. Try login first (existing user).
    2. If login fails, try signup.
    3. If email confirmation is needed, return status.
    4. Otherwise generate SDK key, save to disk.

    Returns only the key prefix in the response (never the full key).
    The full key is saved to ``~/.horizon/credentials.json`` (encrypted).
    """
    # Try login first
    result = login(email, password)
    if "error" in result:
        # Login failed -- try signup
        result = signup(email, password, name)
        if "error" in result:
            return result
        if result.get("status") == "confirmation_required":
            return result

    access_token = result["access_token"]

    # Generate and insert SDK key
    key_result = create_sdk_key(access_token, name=name or "default")
    if "error" in key_result:
        return key_result

    api_key = key_result["key"]
    prefix = key_result["prefix"]

    # Save locally (encrypted)
    path = save_credentials(api_key)

    # Return only the prefix -- never the full key in tool responses
    return {
        "status": "ok",
        "prefix": prefix,
        "credentials_path": path,
        "message": (
            f"SDK key saved (encrypted) to {path}. "
            f"Key prefix: {prefix}... "
            "The engine will auto-load from this file, or set HORIZON_API_KEY env var."
        ),
    }
