"""Tenant and API key registry helpers for the HIEROMEM API."""

from __future__ import annotations

from dataclasses import dataclass, field
import os
from datetime import datetime
from functools import lru_cache
from pathlib import Path
from threading import Lock
from typing import Optional, Tuple
import sqlite3
import hashlib

from hieromem_api.settings import get_settings

try:
    from cryptography.fernet import Fernet
except ImportError:
    Fernet = None  # type: ignore


@dataclass(slots=True)
class Tenant:
    """Simple representation of a tenant/API key record."""

    tenant_id: str
    api_key_hash: str
    api_key: str
    status: str = "active"
    role: str = "admin"  # admin, read_write, read_only
    tier: str = "free"  # free, pro, team, enterprise
    rate_limit_per_minute: Optional[int] = None
    max_episodes: Optional[int] = None
    max_namespaces: Optional[int] = None
    allow_unauthenticated: bool = False
    enforce_namespace_isolation: bool = True
    stripe_customer_id: Optional[str] = None
    stripe_subscription_id: Optional[str] = None
    password_hash: Optional[str] = None
    name: Optional[str] = None
    id: Optional[int] = None
    created_at: datetime = field(default_factory=datetime.utcnow)
    updated_at: datetime = field(default_factory=datetime.utcnow)
    expires_at: Optional[datetime] = None
    revoked_at: Optional[datetime] = None

    def is_active(self) -> bool:
        if self.status.lower() != "active":
            return False
        now = datetime.utcnow()
        if self.revoked_at and self.revoked_at <= now:
            return False
        if self.expires_at and self.expires_at <= now:
            return False
        return True


class BaseTenantRegistry:
    """Interface for tenant registries."""

    def get_by_hash(self, api_key_hash: str | None) -> Tenant | None:
        raise NotImplementedError  # pragma: no cover - interface

    def get_by_api_key(self, api_key: str | None) -> Tenant | None:
        raise NotImplementedError  # pragma: no cover - interface

    def upsert(self, tenant: Tenant) -> Tenant:
        raise NotImplementedError  # pragma: no cover - interface

    def clear(self) -> None:
        raise NotImplementedError  # pragma: no cover - interface

    def default_tenant(self) -> Tenant:
        raise NotImplementedError

    def has_api_keys(self) -> bool:
        raise NotImplementedError

    def list_all(self) -> list[Tenant]:
        raise NotImplementedError

    def list_by_tenant(self, tenant_id: str) -> list[Tenant]:
        raise NotImplementedError

    def revoke_key(self, api_key_hash: str) -> None:
        raise NotImplementedError


class InMemoryTenantRegistry(BaseTenantRegistry):
    """Lightweight registry useful for tests and local development."""

    def __init__(self) -> None:
        self._tenants_by_hash: dict[str, Tenant] = {}
        self._tenants_by_key: dict[str, Tenant] = {}
        self._lock = Lock()
        self._default: Tenant | None = None

    def get_by_hash(self, api_key_hash: str | None) -> Tenant | None:
        if not api_key_hash:
            return None
        return self._tenants_by_hash.get(api_key_hash)

    def get_by_api_key(self, api_key: str | None) -> Tenant | None:
        if not api_key:
            return None
        return self._tenants_by_key.get(api_key)

    def upsert(self, tenant: Tenant) -> Tenant:
        with self._lock:
            self._tenants_by_hash[tenant.api_key_hash] = tenant
            self._tenants_by_key[tenant.api_key] = tenant
            if tenant.allow_unauthenticated:
                self._default = tenant
        return tenant

    def clear(self) -> None:
        with self._lock:
            self._tenants_by_hash.clear()
            self._tenants_by_key.clear()
            self._default = None

    def has_api_keys(self) -> bool:
        """Check if any tenants have API keys configured."""
        # Only consider tenants with actual API keys (ignore default/empty)
        return any(key for key in self._tenants_by_key if key)

    def list_all(self) -> list[Tenant]:
        with self._lock:
            return list(self._tenants_by_hash.values())

    def list_by_tenant(self, tenant_id: str) -> list[Tenant]:
        with self._lock:
            return [t for t in self._tenants_by_hash.values() if t.tenant_id == tenant_id]

    def revoke_key(self, api_key_hash: str) -> None:
        with self._lock:
            tenant = self._tenants_by_hash.get(api_key_hash)
            if tenant is None:
                return
            tenant.status = "revoked"
            tenant.revoked_at = datetime.utcnow()

    def default_tenant(self) -> Tenant:
        """Return default tenant, creating one if needed."""
        if self._default:
            return self._default

        # If no default set, try to find one
        if self._tenants_by_hash:
            # Return first tenant
            return next(iter(self._tenants_by_hash.values()))

        # Create a default tenant for testing/development
        default = Tenant(
            tenant_id="default",
            api_key="",
            api_key_hash="",
            allow_unauthenticated=True,
            enforce_namespace_isolation=False,
        )
        self.upsert(default)
        return default


class EncryptionService:
    """Handles encryption and decryption of sensitive data."""

    def __init__(self, key: str | None = None):
        if Fernet is None:
            self._fernet = None
            return

        if not key:
            # Prefer managed secret distribution
            settings = get_settings()
            key = settings.encryption_key or os.getenv("HIEROMEM_ENCRYPTION_KEY")
            if not key:
                self._fernet = None
                return

        self._fernet = Fernet(key)

    def encrypt(self, data: str) -> str:
        if not self._fernet:
            return data
        return self._fernet.encrypt(data.encode()).decode()

    def decrypt(self, token: str) -> str:
        if not self._fernet:
            return token
        try:
            return self._fernet.decrypt(token.encode()).decode()
        except Exception:
            # Fallback if decryption fails (e.g. key changed or data wasn't
            # encrypted)
            return token

    def default_tenant(self) -> Tenant:
        if self._default is None:
            self._default = Tenant(
                tenant_id="public",
                api_key_hash="",
                api_key="",
                allow_unauthenticated=True,
                enforce_namespace_isolation=False,
            )
        return self._default

    def has_api_keys(self) -> bool:
        return bool(self._tenants_by_hash)


class SqliteTenantRegistry(BaseTenantRegistry):
    """SQLite-backed tenant registry for simple persistence."""

    def __init__(self, path: str) -> None:
        self._path = Path(path)
        self._path.parent.mkdir(parents=True, exist_ok=True)
        self._lock = Lock()
        self._encryption = EncryptionService()
        self._init_db()

    def _connect(self) -> sqlite3.Connection:
        return sqlite3.connect(self._path)

    def _init_db(self) -> None:
        with self._connect() as conn:
            conn.executescript(
                """
                CREATE TABLE IF NOT EXISTS tenants (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    tenant_id TEXT NOT NULL,
                    api_key_hash TEXT NOT NULL UNIQUE,
                    api_key TEXT NOT NULL UNIQUE,
                    status TEXT NOT NULL,
                    role TEXT NOT NULL DEFAULT 'admin',
                    tier TEXT NOT NULL DEFAULT 'free',
                    rate_limit_per_minute INTEGER,
                    max_episodes INTEGER,
                    max_namespaces INTEGER,
                    allow_unauthenticated INTEGER NOT NULL DEFAULT 0,
                    enforce_namespace_isolation INTEGER NOT NULL DEFAULT 1,
                    stripe_customer_id TEXT,
                    stripe_subscription_id TEXT,
                    created_at TEXT NOT NULL,
                    updated_at TEXT NOT NULL,
                    expires_at TEXT,
                    revoked_at TEXT
                );
                CREATE INDEX IF NOT EXISTS idx_tenants_tenant_id ON tenants(tenant_id);
                CREATE INDEX IF NOT EXISTS idx_tenants_stripe_customer ON tenants(stripe_customer_id);
                """
            )
            # Handle migration separately since try/catch block isn't valid SQL script
            try:
                conn.execute("ALTER TABLE tenants ADD COLUMN password_hash TEXT")
            except sqlite3.OperationalError:
                pass  # Column likely exists

            # Handle migration separately since try/catch block isn't valid SQL script
            try:
                conn.execute("ALTER TABLE tenants ADD COLUMN password_hash TEXT")
            except sqlite3.OperationalError:
                pass  # Column likely exists

            try:
                conn.execute("ALTER TABLE tenants ADD COLUMN name TEXT")
            except sqlite3.OperationalError:
                pass  # Column likely exists

            conn.commit()

    def _row_columns(self) -> str:
        return (
            "tenant_id, api_key_hash, api_key, status, role, tier, "
            "rate_limit_per_minute, max_episodes, max_namespaces, "
            "allow_unauthenticated, enforce_namespace_isolation, "
            "stripe_customer_id, stripe_subscription_id, password_hash, name, "
            "created_at, updated_at, expires_at, revoked_at, id"
        )

    def _row_to_tenant(
        self,
        row: Tuple[
            str,
            str,
            str,
            str,
            str,
            str,
            Optional[int],
            Optional[int],
            Optional[int],
            int,
            int,
            Optional[str],
            Optional[str],
            str,
            str,
            Optional[str],
            Optional[str],
            int,
        ],
    ) -> Tenant:
        (
            tenant_id,
            api_key_hash,
            api_key,
            status,
            role,
            tier,
            rate_limit_per_minute,
            max_episodes,
            max_namespaces,
            allow_unauthenticated,
            enforce_namespace_isolation,
            stripe_customer_id,
            stripe_subscription_id,
            password_hash,
            name,
            created_at,
            updated_at,
            expires_at,
            revoked_at,
            tenant_pk,
        ) = row
        return Tenant(
            tenant_id=tenant_id,
            api_key_hash=api_key_hash,
            api_key=api_key,
            status=status,
            role=role,
            tier=tier or "free",
            rate_limit_per_minute=rate_limit_per_minute,
            max_episodes=max_episodes,
            max_namespaces=max_namespaces,
            allow_unauthenticated=bool(allow_unauthenticated),
            enforce_namespace_isolation=bool(enforce_namespace_isolation),
            stripe_customer_id=stripe_customer_id,
            stripe_subscription_id=stripe_subscription_id,
            password_hash=password_hash,
            name=name,
            created_at=datetime.fromisoformat(created_at),
            updated_at=datetime.fromisoformat(updated_at),
            expires_at=(
                datetime.fromisoformat(expires_at) if expires_at else None
            ),
            revoked_at=(
                datetime.fromisoformat(revoked_at) if revoked_at else None
            ),
            id=tenant_pk,
        )

    def _row_to_tenant_decrypted(self, row: Tuple) -> Tenant:
        tenant = self._row_to_tenant(row)
        # Decrypt the API key
        tenant.api_key = self._encryption.decrypt(tenant.api_key)
        return tenant

    def get_by_hash(self, api_key_hash: str | None) -> Tenant | None:
        if not api_key_hash:
            return None
        with self._connect() as conn:
            query = "SELECT " + self._row_columns() + " FROM tenants WHERE api_key_hash = ?"  # nosec B608
            row = conn.execute(query, (api_key_hash,)).fetchone()
        if row is None:
            return None
        return self._row_to_tenant_decrypted(row)

    def get_by_api_key(self, api_key: str | None) -> Tenant | None:
        if not api_key:
            return None
        # Cannot search by encrypted api_key, so search by hash
        key_hash = hashlib.sha256(api_key.encode("utf-8")).hexdigest()
        return self.get_by_hash(key_hash)

    def upsert(self, tenant: Tenant) -> Tenant:
        now = datetime.utcnow().isoformat()
        with self._lock:
            with self._connect() as conn:
                conn.execute(
                    """
                    INSERT INTO tenants (
                        tenant_id, api_key_hash, api_key, status, role, tier,
                        rate_limit_per_minute, max_episodes, max_namespaces,
                        allow_unauthenticated, enforce_namespace_isolation,
                        stripe_customer_id, stripe_subscription_id, password_hash, name,
                        created_at, updated_at, expires_at, revoked_at
                    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                    ON CONFLICT(api_key_hash) DO UPDATE SET
                        tenant_id=excluded.tenant_id,
                        api_key=excluded.api_key,
                        status=excluded.status,
                        role=excluded.role,
                        tier=excluded.tier,
                        rate_limit_per_minute=excluded.rate_limit_per_minute,
                        max_episodes=excluded.max_episodes,
                        max_namespaces=excluded.max_namespaces,
                        allow_unauthenticated=excluded.allow_unauthenticated,
                        enforce_namespace_isolation=excluded.enforce_namespace_isolation,
                        stripe_customer_id=excluded.stripe_customer_id,
                        stripe_subscription_id=excluded.stripe_subscription_id,
                        password_hash=excluded.password_hash,
                        name=excluded.name,
                        updated_at=excluded.updated_at,
                        expires_at=excluded.expires_at,
                        revoked_at=excluded.revoked_at
                    """,
                    (
                        tenant.tenant_id,
                        tenant.api_key_hash,
                        self._encryption.encrypt(tenant.api_key),
                        tenant.status,
                        tenant.role,
                        tenant.tier,
                        tenant.rate_limit_per_minute,
                        tenant.max_episodes,
                        tenant.max_namespaces,
                        int(tenant.allow_unauthenticated),
                        int(tenant.enforce_namespace_isolation),
                        tenant.stripe_customer_id,
                        tenant.stripe_subscription_id,
                        tenant.password_hash,
                        tenant.name,
                        now,
                        now,
                        (
                            tenant.expires_at.isoformat()
                            if tenant.expires_at
                            else None
                        ),
                        (
                            tenant.revoked_at.isoformat()
                            if tenant.revoked_at
                            else None
                        ),
                    ),
                )
                conn.commit()
        return tenant

    def clear(self) -> None:
        with self._connect() as conn:
            conn.execute("DELETE FROM tenants")
            conn.commit()

    def default_tenant(self) -> Tenant:
        with self._connect() as conn:
            query = "SELECT " + self._row_columns() + " FROM tenants LIMIT 1"  # nosec B608
            row = conn.execute(query).fetchone()

        if row:
            return self._row_to_tenant_decrypted(row)

        return Tenant(
            tenant_id="public",
            api_key_hash="",
            api_key="",
            allow_unauthenticated=True,
            enforce_namespace_isolation=False,
        )

    def has_api_keys(self) -> bool:
        with self._connect() as conn:
            row = conn.execute(
                "SELECT COUNT(1) FROM tenants WHERE api_key_hash != ''"
            ).fetchone()
        return bool(row and row[0])

    def list_all(self) -> list[Tenant]:
        with self._connect() as conn:
            query = "SELECT " + self._row_columns() + " FROM tenants"  # nosec B608
            rows = conn.execute(query).fetchall()
        return [self._row_to_tenant_decrypted(row) for row in rows]

    def list_by_tenant(self, tenant_id: str) -> list[Tenant]:
        with self._connect() as conn:
            query = "SELECT " + self._row_columns() + " FROM tenants WHERE tenant_id = ?"  # nosec B608
            rows = conn.execute(query, (tenant_id,)).fetchall()
        return [self._row_to_tenant_decrypted(row) for row in rows]

    def revoke_key(self, api_key_hash: str) -> None:
        now = datetime.utcnow().isoformat()
        with self._connect() as conn:
            conn.execute(
                "UPDATE tenants SET status = 'revoked', revoked_at = ? WHERE api_key_hash = ?",
                (now, api_key_hash),
            )
            conn.commit()


def _build_registry() -> BaseTenantRegistry:
    settings = get_settings()
    if settings.tenant_registry_path:
        return SqliteTenantRegistry(settings.tenant_registry_path)
    registry = InMemoryTenantRegistry()

    default_api_key = os.getenv("HIEROMEM_API_KEY", "test-key")
    if default_api_key:
        key_hash = hashlib.sha256(default_api_key.encode("utf-8")).hexdigest()
        registry.upsert(
            Tenant(
                tenant_id="public",
                api_key=default_api_key,
                api_key_hash=key_hash,
                allow_unauthenticated=True,
                enforce_namespace_isolation=False,
            )
        )

    return registry


@lru_cache(maxsize=1)
def get_tenant_registry() -> BaseTenantRegistry:
    return _build_registry()


def resolve_tenant(tenant: Tenant | object) -> Tenant:
    """Return a concrete Tenant even when dependency injection is bypassed."""

    if isinstance(tenant, Tenant):
        return tenant
    registry = get_tenant_registry()
    return registry.default_tenant()


def canonical_namespace(tenant: Tenant | object, namespace: str | None) -> str:
    """Return the internal namespace for a tenant-scoped request."""

    tenant_obj = resolve_tenant(tenant)
    clean = (namespace or "default").strip() or "default"
    if not tenant_obj.enforce_namespace_isolation:
        return clean
    return f"{tenant_obj.tenant_id}::{clean}"


def public_namespace(
    tenant: Tenant | object, namespace: str | None
) -> str | None:
    """Strip tenant prefix when returning namespaces to the caller."""

    tenant_obj = resolve_tenant(tenant)
    if namespace is None:
        return None
    if not tenant_obj.enforce_namespace_isolation:
        return namespace
    prefix = f"{tenant_obj.tenant_id}::"
    if namespace.startswith(prefix):
        return namespace[len(prefix):]
    return namespace


__all__ = [
    "Tenant",
    "BaseTenantRegistry",
    "get_tenant_registry",
    "canonical_namespace",
    "public_namespace",
    "resolve_tenant",
]
