"""
BIBIVIDI — License key management
Handles local storage, validation, and hashing of the user's license key.

Key format: bvd-xxxxxxxx (UUID-style, issued by Stripe webhook on bibividi.ai)
Storage:    ~/.bibividi/config.toml  [auth] license_key = "bvd-..."
Env override: BIBIVIDI_LICENSE_KEY (for CI / Docker)
"""
from __future__ import annotations

import hashlib
import logging
import os
import re
from pathlib import Path

logger = logging.getLogger(__name__)

_KEY_RE = re.compile(r"^bvd-[a-z0-9\-]{6,}$")

# Patchable in tests via patch("bvd.license.BIBIVIDI_DATA_DIR", ...)
BIBIVIDI_DATA_DIR: Path = Path.home() / ".bibividi"


def is_valid_format(key: str) -> bool:
    """Return True if the key matches the bvd-xxxxxxxx format."""
    return bool(_KEY_RE.match(key.strip()))


def hash_license_key(key: str) -> str:
    """Return the SHA-256 hex digest of the key (stored in audit trail, not the key itself)."""
    return hashlib.sha256(key.strip().encode()).hexdigest()


def load_license_key(config_dir: Path | None = None) -> str | None:
    """Return the license key from env var or config.toml, or None if not set.

    Args:
        config_dir: override the ~/.bibividi directory (used in tests).
    """
    # 1. Env var override (CI / Docker)
    env_key = os.environ.get("BIBIVIDI_LICENSE_KEY", "").strip()
    if env_key:
        return env_key

    # 2. config.toml — use config_dir if provided, else BIBIVIDI_DATA_DIR
    path = (config_dir or BIBIVIDI_DATA_DIR) / "config.toml"
    if not path.exists():
        return None

    try:
        import tomllib  # Python 3.11+
        data = tomllib.loads(path.read_text())
        return data.get("auth", {}).get("license_key") or None
    except Exception as exc:
        logger.debug("Could not read config.toml: %s", exc)
        return None


def save_license_key(key: str, config_dir: Path | None = None) -> None:
    """Persist the license key to config.toml.

    Args:
        key: the license key to save.
        config_dir: override the ~/.bibividi directory (used in tests).
    """
    path = (config_dir or BIBIVIDI_DATA_DIR) / "config.toml"
    path.parent.mkdir(parents=True, exist_ok=True)

    # Read existing content; replace or add [auth] section cleanly
    existing = path.read_text() if path.exists() else ""
    lines = existing.splitlines()

    in_auth = False
    filtered: list[str] = []
    for line in lines:
        if line.strip() == "[auth]":
            in_auth = True
            filtered.append(line)
            continue
        if line.startswith("[") and line.strip() != "[auth]":
            in_auth = False
        if in_auth and line.strip().startswith("license_key"):
            continue  # drop old value
        filtered.append(line)

    content = "\n".join(filtered).rstrip()

    if "[auth]" not in content:
        content = (content + "\n\n[auth]\n").lstrip("\n")

    content = content.rstrip() + f'\nlicense_key = "{key.strip()}"\n'
    path.write_text(content)


async def validate_key_with_api(key: str, api_url: str) -> bool:
    """Call the billing API to verify the key is valid and active.

    Returns:
        True  — key is valid (200 response or API unreachable / offline mode)
        False — key rejected (401/403/404)

    Raises:
        Any non-httpx exception (e.g. OSError) is re-raised to the caller so
        that `bibividi activate` can surface unexpected errors rather than
        silently accepting a broken key.
    """
    import httpx

    url = f"{api_url.rstrip('/')}/v1/keys/validate"
    try:
        async with httpx.AsyncClient(timeout=5.0) as client:
            resp = await client.get(url, headers={"X-BIBIVIDI-License": key.strip()})
        if resp.status_code == 200:
            return True
        logger.warning("Key validation returned %d", resp.status_code)
        return False
    except httpx.ConnectError:
        # Billing API not yet deployed — save key in offline mode, validated server-side later
        logger.info("Billing API unreachable — saving key in offline mode")
        return True
    # All other exceptions (OSError, etc.) propagate to the caller
