"""Managed API key CRUD operations with DynamoDB."""

import logging
import os
from datetime import datetime, timedelta
from typing import TYPE_CHECKING, Any

try:
    import boto3

    HAS_BOTO3 = True
except ImportError:
    boto3 = None  # type: ignore[assignment]
    HAS_BOTO3 = False

if TYPE_CHECKING:
    from mypy_boto3_dynamodb.service_resource import Table

from ai_lls_lib.key_management import (
    compute_key_hash,
    generate_key_id,
    generate_managed_key,
    validate_expiration_days,
)

logger = logging.getLogger(__name__)

MAX_ACTIVE_KEYS = 10
MAX_LABEL_LENGTH = 64
REVOKE_TTL_DAYS = 30


class KeyNotFoundError(ValueError):
    """Raised when a managed API key is not found."""


class RevokedKeyError(ValueError):
    """Raised when attempting to modify a revoked key."""


class LimitExceededError(ValueError):
    """Raised when the active key limit is reached."""


class ManagedApiKeyService:
    """Manages user API keys with CRUD operations in DynamoDB.

    DynamoDB table schema:
        - Hash key: user_id (S)
        - Range key: key_id (S)
    """

    table: "Table | None"

    def __init__(self, table_name: str | None = None):
        """Initialize with DynamoDB table."""
        if not HAS_BOTO3 or not boto3:
            raise RuntimeError("boto3 is required for ManagedApiKeyService")

        self.dynamodb = boto3.resource("dynamodb")
        self.table_name = table_name if table_name else os.environ["MANAGED_API_KEYS_TABLE"]

        try:
            self.table = self.dynamodb.Table(self.table_name)
        except Exception as e:
            logger.error(f"Failed to connect to DynamoDB table {self.table_name}: {e}")
            self.table = None

    def _get_key(self, user_id: str, key_id: str) -> dict[str, Any]:
        """Fetch a key item, raising if not found or revoked."""
        if not self.table:
            raise RuntimeError(f"DynamoDB table {self.table_name} not accessible")

        response = self.table.get_item(Key={"user_id": user_id, "key_id": key_id})
        item = response.get("Item")
        if not item:
            raise KeyNotFoundError(f"Key {key_id} not found for user {user_id}")
        if item.get("status") == "revoked":
            raise RevokedKeyError(f"Key {key_id} is revoked")
        return item

    def list_keys(self, user_id: str) -> list[dict[str, Any]]:
        """List all API keys for a user, sorted by created_at descending.

        Returns projected fields only (excludes key_hash).
        """
        if not self.table:
            raise RuntimeError(f"DynamoDB table {self.table_name} not accessible")

        response = self.table.query(
            KeyConditionExpression="user_id = :uid",
            ExpressionAttributeValues={":uid": user_id},
        )
        items = response.get("Items", [])

        result = []
        for item in items:
            result.append(
                {
                    "key_id": item["key_id"],
                    "key_last4": item.get("key_last4", ""),
                    "label": item.get("label", ""),
                    "status": item.get("status", "active"),
                    "created_at": item.get("created_at", ""),
                    "expires_at": item.get("expires_at"),
                    "last_used_at": item.get("last_used_at"),
                }
            )

        result.sort(key=lambda x: str(x.get("created_at", "")), reverse=True)
        return result

    def create_key(self, user_id: str, label: str, expires_in_days: int = 365) -> dict[str, Any]:
        """Create a new managed API key.

        Returns the key_id and plaintext key (only time key is returned).
        """
        if not self.table:
            raise RuntimeError(f"DynamoDB table {self.table_name} not accessible")

        # Validate label
        label = label.strip()
        if not label or len(label) > MAX_LABEL_LENGTH:
            raise ValueError(f"Label must be 1-{MAX_LABEL_LENGTH} characters, got {len(label)}")

        # Validate expiration
        if not validate_expiration_days(expires_in_days):
            raise ValueError(f"Expiration must be 1-730 days, got {expires_in_days}")

        # Check active key count
        existing = self.list_keys(user_id)
        active_count = sum(1 for k in existing if k["status"] != "revoked")
        if active_count >= MAX_ACTIVE_KEYS:
            raise LimitExceededError(f"Maximum of {MAX_ACTIVE_KEYS} active keys reached")

        # Generate key
        key_id = generate_key_id()
        plaintext_key = generate_managed_key()
        key_hash = compute_key_hash(plaintext_key)
        now = datetime.utcnow().isoformat()
        expires_at = (datetime.utcnow() + timedelta(days=expires_in_days)).isoformat()

        self.table.put_item(
            Item={
                "user_id": user_id,
                "key_id": key_id,
                "key_hash": key_hash,
                "key_last4": plaintext_key[-4:],
                "label": label,
                "status": "active",
                "created_at": now,
                "expires_at": expires_at,
                "last_used_at": None,
                "usage_count": 0,
            }
        )

        logger.info(f"Created managed key {key_id} for user {user_id}")
        return {
            "key_id": key_id,
            "api_key": plaintext_key,
            "label": label,
            "expires_at": expires_at,
        }

    def update_key(
        self,
        user_id: str,
        key_id: str,
        label: str | None = None,
        expires_in_days: int | None = None,
    ) -> dict[str, Any]:
        """Update key label and/or expiration."""
        if label is None and expires_in_days is None:
            raise ValueError("At least one of label or expires_in_days must be provided")

        # This will raise KeyNotFoundError or RevokedKeyError
        self._get_key(user_id, key_id)

        update_parts = ["SET updated_at = :now"]
        expr_values: dict[str, Any] = {":now": datetime.utcnow().isoformat()}

        if label is not None:
            label = label.strip()
            if not label or len(label) > MAX_LABEL_LENGTH:
                raise ValueError(f"Label must be 1-{MAX_LABEL_LENGTH} characters")
            update_parts.append("label = :label")
            expr_values[":label"] = label

        if expires_in_days is not None:
            if not validate_expiration_days(expires_in_days):
                raise ValueError(f"Expiration must be 1-730 days, got {expires_in_days}")
            expires_at = (datetime.utcnow() + timedelta(days=expires_in_days)).isoformat()
            update_parts.append("expires_at = :expires_at")
            expr_values[":expires_at"] = expires_at

        update_expr = update_parts[0]
        if len(update_parts) > 1:
            update_expr += ", " + ", ".join(update_parts[1:])

        if not self.table:
            raise RuntimeError(f"DynamoDB table {self.table_name} not accessible")

        self.table.update_item(
            Key={"user_id": user_id, "key_id": key_id},
            UpdateExpression=update_expr,
            ExpressionAttributeValues=expr_values,
        )

        logger.info(f"Updated managed key {key_id} for user {user_id}")
        return {"message": "Key updated"}

    def rotate_key(self, user_id: str, key_id: str) -> dict[str, Any]:
        """Generate a new key value while keeping the same key_id.

        Returns the new plaintext key (only time it's returned).
        """
        # This will raise KeyNotFoundError or RevokedKeyError
        self._get_key(user_id, key_id)

        plaintext_key = generate_managed_key()
        key_hash = compute_key_hash(plaintext_key)
        now = datetime.utcnow().isoformat()

        if not self.table:
            raise RuntimeError(f"DynamoDB table {self.table_name} not accessible")

        self.table.update_item(
            Key={"user_id": user_id, "key_id": key_id},
            UpdateExpression=("SET key_hash = :hash, key_last4 = :last4, updated_at = :now"),
            ExpressionAttributeValues={
                ":hash": key_hash,
                ":last4": plaintext_key[-4:],
                ":now": now,
            },
        )

        logger.info(f"Rotated managed key {key_id} for user {user_id}")
        return {
            "key_id": key_id,
            "api_key": plaintext_key,
            "label": "",
            "expires_at": "",
        }

    def revoke_key(self, user_id: str, key_id: str) -> None:
        """Mark a key as revoked with TTL for automatic cleanup."""
        if not self.table:
            raise RuntimeError(f"DynamoDB table {self.table_name} not accessible")

        # Check key exists (but allow revoking already-revoked keys)
        response = self.table.get_item(Key={"user_id": user_id, "key_id": key_id})
        if not response.get("Item"):
            raise KeyNotFoundError(f"Key {key_id} not found for user {user_id}")

        now = datetime.utcnow()
        ttl = int((now + timedelta(days=REVOKE_TTL_DAYS)).timestamp())

        self.table.update_item(
            Key={"user_id": user_id, "key_id": key_id},
            UpdateExpression=("SET #s = :revoked, revoked_at = :now, #ttl = :ttl"),
            ExpressionAttributeNames={"#s": "status", "#ttl": "ttl"},
            ExpressionAttributeValues={
                ":revoked": "revoked",
                ":now": now.isoformat(),
                ":ttl": ttl,
            },
        )

        logger.info(f"Revoked managed key {key_id} for user {user_id}")
