"""Authentication repositories — API keys, users, and roles.

All persistence is delegated to :class:`BaseRepository` methods — no raw
SQL or SQLAlchemy imports.
"""

from __future__ import annotations

import hashlib
import logging
import secrets
import uuid
from datetime import datetime, timezone
from typing import Any

from repo_core import BaseRepository, hook
from repo_core.domains.authentication.model import ApiKey, AuthUser, AuthRole
from repo_core.domains.authentication.schema import (
    ApiKeyValidationResult,
)

logger = logging.getLogger(__name__)


def _hash_key(raw_key: str) -> str:
    """Hash a raw API key with SHA-256 (no salt — keys are 256-bit random)."""
    return hashlib.sha256(raw_key.encode()).hexdigest()


def hash_password(password: str, iterations: int = 390_000) -> str:
    """Hash password with PBKDF2-HMAC-SHA256.

    Storage format:
        pbkdf2_sha256$<iterations>$<salt_hex>$<hash_hex>
    """
    salt = secrets.token_bytes(16)
    digest = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, iterations)
    return (
        f"pbkdf2_sha256${iterations}${salt.hex()}${digest.hex()}"
    )


def verify_password(password: str, stored_hash: str) -> bool:
    """Verify password against ``hash_password`` output."""
    try:
        algorithm, iter_str, salt_hex, digest_hex = stored_hash.split("$")
        if algorithm != "pbkdf2_sha256":
            return False
        iterations = int(iter_str)
        salt = bytes.fromhex(salt_hex)
        expected = bytes.fromhex(digest_hex)
        actual = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, iterations)
        return secrets.compare_digest(actual, expected)
    except Exception:
        return False


def generate_api_key() -> tuple[str, str]:
    """Generate a raw API key and its hash.

    Returns:
        (raw_key, key_hash) — store key_hash in DB, return raw_key to caller once.
    """
    raw = "sam_" + secrets.token_hex(32)  # 64-char hex + 4-char prefix
    return raw, _hash_key(raw)


class ApiKeyRepository(BaseRepository[ApiKey]):
    """CRUD for API keys via QueryBuilder.

    Usage::

        from db_core import create_postgres_client, QueryBuilder

        pg = create_postgres_client()
        repo = ApiKeyRepository(model=ApiKey, db=QueryBuilder(pg, ""))
        async with repo:
            raw, created = await repo.create_api_key({"name": "my-key", "scopes": ["*"]})
            result = await repo.validate_api_key(raw)
            assert result.valid
    """

    TABLE = "api_keys"

    # -- Hooks -------------------------------------------------------------

    @hook("after_create")
    async def _log_create(self, api_key: ApiKey) -> None:
        logger.info("API key created: id=%s name=%s", api_key.id, api_key.name)

    @hook("after_delete")
    async def _log_delete(self, api_key: ApiKey) -> None:
        logger.info("API key revoked: id=%s name=%s", api_key.id, api_key.name)

    # -- CRUD --------------------------------------------------------------

    async def create_api_key(self, data: dict[str, Any]) -> tuple[str, ApiKey]:
        """Create a new API key.

        Args:
            data: Must include ``name`` (str) and optionally ``scopes`` (list[str]).

        Returns:
            (raw_key, api_key) — the raw key is only returned here; store it safely.
        """
        raw_key, key_hash = generate_api_key()

        record = {
            "name": data["name"],
            "key_hash": key_hash,
            "scopes": data.get("scopes", []),
            "is_active": True,
        }

        await self.emit("before_create", record)
        created = await self.insert_one(self.TABLE, record, returning=["*"])
        await self.emit("after_create", created)

        return raw_key, created

    async def validate_api_key(self, raw_key: str) -> ApiKeyValidationResult:
        """Validate a raw API key string.

        Returns an ``ApiKeyValidationResult`` with validity, scopes, and optional error.
        """
        if not raw_key or not raw_key.startswith("sam_"):
            return ApiKeyValidationResult(valid=False, error="Invalid API key format")

        key_hash = _hash_key(raw_key)

        rows = await self.find(
            self.TABLE,
            conditions={"key_hash": key_hash, "is_active": True},
            limit=1,
            order_by=["-created_at"],
        )

        if not rows:
            return ApiKeyValidationResult(valid=False, error="API key not found or revoked")

        api_key = rows[0]

        # Update last_used_at
        await self.update_by_id(
            self.TABLE,
            api_key.id,
            {"last_used_at": datetime.now(timezone.utc)},
        )

        return ApiKeyValidationResult(
            valid=True,
            api_key_id=api_key.id,
            scopes=api_key.scopes,
        )

    async def revoke_api_key(self, api_key_id: uuid.UUID) -> ApiKey | None:
        """Revoke (deactivate) an API key by ID."""
        await self.emit("before_delete", api_key_id)
        updated = await self.update_by_id(
            self.TABLE, api_key_id, {"is_active": False}
        )
        if updated is not None:
            await self.emit("after_delete", updated)
        return updated

    async def list_api_keys(
        self, limit: int = 50, offset: int = 0
    ) -> tuple[list[ApiKey], int]:
        """List all API keys (paginated)."""
        total = await self.count(self.TABLE)
        rows = await self.all(
            self.TABLE,
            limit=limit,
            offset=offset,
            order_by=["-created_at"],
        )
        return rows, total

    async def get_by_id(self, api_key_id: uuid.UUID) -> ApiKey | None:
        """Get an API key by its UUID."""
        return await self.find_by_id(self.TABLE, api_key_id)


class UserRepository(BaseRepository[AuthUser]):
    """Repository for dashboard users (username/password auth)."""

    TABLE = "auth_users"

    @hook("after_create")
    async def _log_create(self, user: AuthUser) -> None:
        logger.info("Auth user created: id=%s username=%s", user.id, user.username)

    async def create_user(self, data: dict[str, Any]) -> AuthUser:
        """Create a new user with hashed password."""
        username = str(data["username"]).strip().lower()
        email = str(data["email"]).strip().lower()
        role = str(data.get("role", "viewer")).strip().lower()

        role_repo = RoleRepository(model=AuthRole, db=self.db)
        role_obj = await role_repo.get_by_name(role)
        if role_obj is None:
            raise ValueError(f"Role '{role}' does not exist")

        if await self.exists(self.TABLE, {"username": username}):
            raise ValueError("Username already exists")
        if await self.exists(self.TABLE, {"email": email}):
            raise ValueError("Email already exists")

        record = {
            "username": username,
            "email": email,
            "password_hash": hash_password(str(data["password"])),
            "display_name": data.get("display_name"),
            "role": role,
            "is_active": True,
        }
        if "id" in data:
            record["id"] = data["id"]
        await self.emit("before_create", record)
        created = await self.insert_one(self.TABLE, record, returning=["*"])
        await self.emit("after_create", created)
        return created

    async def authenticate_user(
        self, username: str, password: str,
    ) -> AuthUser | None:
        """Authenticate by username/email + password."""
        identity = username.strip().lower()
        user = await self.first(
            self.TABLE,
            {"username": identity, "is_active": True},
        )
        if user is None:
            user = await self.first(
                self.TABLE,
                {"email": identity, "is_active": True},
            )
        if user is None:
            return None
        if not verify_password(password, user.password_hash):
            return None

        await self.update_by_id(
            self.TABLE,
            user.id,
            {"last_login_at": datetime.now(timezone.utc)},
        )
        return await self.find_by_id(self.TABLE, user.id)

    async def get_by_id(self, user_id: uuid.UUID) -> AuthUser | None:
        return await self.find_by_id(self.TABLE, user_id)


class RoleRepository(BaseRepository[AuthRole]):
    """Repository for role definitions and seeded scopes."""

    TABLE = "auth_roles"

    async def get_by_name(self, name: str) -> AuthRole | None:
        return await self.first(self.TABLE, {"name": name, "is_active": True})

    async def create_role(self, data: dict[str, Any]) -> AuthRole:
        existing = await self.first(self.TABLE, {"name": data["name"]})
        if existing is not None:
            return existing
        insert_data = {
            "name": data["name"],
            "description": data.get("description"),
            "scopes": data.get("scopes", []),
            "is_active": True,
        }
        if "id" in data:
            insert_data["id"] = data["id"]
        return await self.insert_one(
            self.TABLE,
            insert_data,
            returning=["*"],
        )

    async def list_roles(self) -> list[AuthRole]:
        return await self.all(self.TABLE, order_by=["name"])


# Backward-compatible alias used by existing app code
AuthenticationRepository = ApiKeyRepository
