import hashlib
import hmac
import secrets
import time
import logging
from datetime import datetime, timezone
from typing import Any

from fastapi import HTTPException, Request, status
from sqlalchemy import select
from sqlalchemy.orm import Session

from src.apps.developer.models.merchant_api_key import MerchantApiKey
from src.apps.developer.models.api_request_log import ApiRequestLog
from src.apps.merchants.models.merchant import Merchant

logger = logging.getLogger(__name__)


def _hash_key(raw_key: str, salt: str) -> str:
    """SHA-256(raw_key + salt), hex-encoded."""
    return hashlib.sha256(f"{raw_key}{salt}".encode()).hexdigest()


def generate_api_key(environment: str) -> tuple[str, str, str, str]:
    """
    Generate a new API key.
    Returns (raw_key, key_hash, key_salt, key_prefix).
    """
    prefix = "sk_live_" if environment == "live" else "sk_test_"
    raw_suffix = secrets.token_urlsafe(32)  # 43 chars base64url
    raw_key = f"{prefix}{raw_suffix}"
    salt = secrets.token_hex(16)
    key_hash = _hash_key(raw_key, salt)
    key_prefix = raw_key[:10]
    return raw_key, key_hash, salt, key_prefix


def generate_key_id() -> str:
    """Returns kid_<8-char hex>."""
    return f"kid_{secrets.token_hex(4)}"


def generate_webhook_secret() -> tuple[str, str]:
    """
    Returns (raw_secret, encrypted_secret).
    raw_secret format: whsec_<43-char base64url>
    """
    from src.apps.payment_providers.helpers.credentials import encrypt_credential
    raw = f"whsec_{secrets.token_urlsafe(32)}"
    return raw, encrypt_credential(raw)


async def get_current_merchant_via_api_key(
    request: Request,
    db: Session,
    required_scope: str = "",
) -> tuple[Merchant, MerchantApiKey]:
    """
    Validates API key from Authorization: Bearer sk_xxx OR X-API-Key: sk_xxx header.
    Raises HTTP 401/403 on failure.
    Records ApiRequestLog entry.
    """
    start_time = time.monotonic()

    # Extract raw key from headers
    raw_key: str | None = None
    auth_header = request.headers.get("Authorization", "")
    if auth_header.startswith("Bearer sk_"):
        raw_key = auth_header[len("Bearer "):]
    elif request.headers.get("X-API-Key", "").startswith("sk_"):
        raw_key = request.headers["X-API-Key"]

    if not raw_key:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="API key required")

    # Determine environment from prefix
    if raw_key.startswith("sk_live_"):
        env = "live"
    elif raw_key.startswith("sk_test_"):
        env = "test"
    else:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid API key format")

    # Look up by key_prefix to narrow candidates, then verify hash
    key_prefix = raw_key[:10]
    candidates = db.execute(
        select(MerchantApiKey).where(
            MerchantApiKey.key_prefix == key_prefix,
            MerchantApiKey.environment == env,
            MerchantApiKey.is_active == True,
            MerchantApiKey.revoked_at.is_(None),
        )
    ).scalars().all()

    api_key: MerchantApiKey | None = None
    for candidate in candidates:
        if hmac.compare_digest(_hash_key(raw_key, candidate.key_salt), candidate.key_hash):
            api_key = candidate
            break

    if not api_key:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid or revoked API key")

    # Check expiry
    if api_key.expires_at and api_key.expires_at < datetime.now(timezone.utc):
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="API key has expired")

    # Check scope
    if required_scope and required_scope not in (api_key.scopes or []):
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail=f"Insufficient scope: {required_scope} required",
        )

    # Load merchant
    merchant = db.execute(
        select(Merchant).where(Merchant.id == api_key.merchant_id, Merchant.deleted_at.is_(None))
    ).scalar_one_or_none()
    if not merchant:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Associated merchant not found")

    # Store on request state for middleware/background tasks
    request.state.auth_method = "api_key"
    request.state.api_key_id = api_key.key_id
    request.state.api_key_db_id = api_key.id
    request.state.api_key_scopes = api_key.scopes or []
    request.state.api_key_environment = env
    request.state.merchant_id_val = merchant.id

    # Background: update last_used_at
    def _update_last_used(key_id_val: int) -> None:
        try:
            from src.core.database import SessionCelery
            from sqlalchemy import update
            with SessionCelery() as bg_db:
                bg_db.execute(
                    update(MerchantApiKey)
                    .where(MerchantApiKey.id == key_id_val)
                    .values(last_used_at=datetime.now(timezone.utc))
                )
                bg_db.commit()
        except Exception as exc:
            logger.warning("Failed to update last_used_at for key %d: %s", key_id_val, exc)

    import asyncio
    asyncio.get_running_loop().run_in_executor(None, _update_last_used, api_key.id)

    return merchant, api_key


class RequireScope:
    """
    FastAPI dependency class for scope-based authorization.

    For JWT-authenticated requests: scope check is skipped.
    For API key requests: verifies required_scope in api_key.scopes; raises HTTP 403 if absent.
    """

    def __init__(self, required_scope: str):
        self.required_scope = required_scope

    async def __call__(self, request: Request) -> None:
        auth_method = getattr(request.state, "auth_method", "jwt")
        if auth_method != "api_key":
            return  # JWT — full access, no scope check needed

        api_key_scopes: list = getattr(request.state, "api_key_scopes", [])
        if self.required_scope not in api_key_scopes:
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail=f"Insufficient scope: {self.required_scope} required",
            )
