from typing import Any

from cryptography.fernet import InvalidToken
from fastapi import APIRouter, HTTPException, Query, status
from pydantic import ValidationError
from sqlalchemy.exc import IntegrityError

from tracecat.auth.dependencies import OrgActorRole, WorkspaceActorRouteRole
from tracecat.authz.controls import require_scope
from tracecat.db.dependencies import AsyncDBSession
from tracecat.exceptions import TracecatNotFoundError
from tracecat.identifiers import SecretID
from tracecat.integrations.aws_assume_role import (
    build_workspace_external_id,
    get_tracecat_aws_account_id,
    get_tracecat_aws_principal_arn,
)
from tracecat.logger import logger
from tracecat.registry.actions.service import RegistryActionsService
from tracecat.secrets.dependencies import AnySecretIDPath
from tracecat.secrets.enums import SecretType
from tracecat.secrets.schemas import (
    AwsAssumeRoleAccessRead,
    OrganizationSecretRead,
    SecretCreate,
    SecretDefinition,
    SecretRead,
    SecretReadMinimal,
    SecretSearch,
    SecretUpdate,
)
from tracecat.secrets.service import SecretsService

router = APIRouter(prefix="/secrets", tags=["secrets"])
org_router = APIRouter(prefix="/organization/secrets", tags=["organization-secrets"])


def _serialize_secret_read_minimal(
    *,
    service: SecretsService,
    secret: Any,
) -> SecretReadMinimal:
    try:
        keys = [kv.key for kv in service.decrypt_keys(secret.encrypted_keys)]
        is_corrupted = False
    except (InvalidToken, ValidationError, ValueError) as e:
        keys = []
        is_corrupted = True
        logger.warning(
            "Failed to decrypt secret keys; returning secret without keys",
            secret_id=str(secret.id),
            secret_name=secret.name,
            secret_type=secret.type,
            error=str(e),
        )

    return SecretReadMinimal(
        id=secret.id,
        type=SecretType(secret.type),
        name=secret.name,
        description=secret.description,
        keys=keys,
        environment=secret.environment,
        is_corrupted=is_corrupted,
    )


@router.get("/search", response_model=list[SecretRead])
@require_scope("secret:read")
async def search_secrets(
    *,
    role: WorkspaceActorRouteRole,
    session: AsyncDBSession,
    environment: str = Query(...),
    names: set[str] | None = Query(
        None, alias="name", description="Filter by secret name"
    ),
    ids: set[SecretID] | None = Query(
        None, alias="id", description="Filter by secret ID"
    ),
    types: set[SecretType] | None = Query(
        None, alias="type", description="Filter by secret type"
    ),
) -> list[SecretRead]:
    """Search secrets."""
    service = SecretsService(session, role=role)
    params: dict[str, Any] = {"environment": environment}
    if names:
        params["names"] = names
    if ids:
        params["ids"] = ids
    if types:
        params["types"] = types
    secrets = await service.search_secrets(SecretSearch(**params))
    decrypted = []
    for secret in secrets:
        decrypted.extend(service.decrypt_keys(secret.encrypted_keys))
    return [SecretRead.from_database(secret) for secret in secrets]


@router.get("")
@require_scope("secret:read")
async def list_secrets(
    *,
    role: WorkspaceActorRouteRole,
    session: AsyncDBSession,
    types: set[SecretType] | None = Query(
        None, alias="type", description="Filter by secret type"
    ),
) -> list[SecretReadMinimal]:
    """List user secrets."""
    service = SecretsService(session, role=role)
    secrets = await service.list_secrets(types=types)
    return [
        _serialize_secret_read_minimal(service=service, secret=secret)
        for secret in secrets
    ]


@router.get("/definitions", response_model=list[SecretDefinition])
@require_scope("secret:read")
async def list_secret_definitions(
    *,
    role: WorkspaceActorRouteRole,
    session: AsyncDBSession,
) -> list[SecretDefinition]:
    """List aggregated secret definitions from the registry."""
    service = RegistryActionsService(session, role=role)
    definitions = await service.get_aggregated_secrets()
    return sorted(
        definitions,
        key=lambda definition: (-definition.action_count, definition.name),
    )


@router.get("/aws-assume-role", response_model=AwsAssumeRoleAccessRead)
@require_scope("secret:read")
async def get_aws_assume_role_access(
    *,
    role: WorkspaceActorRouteRole,
) -> AwsAssumeRoleAccessRead:
    """Get workspace-scoped AWS AssumeRole details for credential setup."""
    workspace_id = role.workspace_id
    if workspace_id is None:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Workspace context is required",
        )

    try:
        return AwsAssumeRoleAccessRead(
            tracecat_aws_account_id=get_tracecat_aws_account_id(),
            tracecat_aws_principal_arn=get_tracecat_aws_principal_arn(),
            external_id=build_workspace_external_id(workspace_id),
        )
    except ValueError as exc:
        raise HTTPException(
            status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
            detail="AWS AssumeRole access is not available right now.",
        ) from exc


@router.get("/{secret_name}")
@require_scope("secret:read")
async def get_secret_by_name(
    *,
    role: WorkspaceActorRouteRole,
    session: AsyncDBSession,
    secret_name: str,
) -> SecretRead:
    """Get a secret."""

    service = SecretsService(session, role=role)
    try:
        secret = await service.get_secret_by_name(secret_name)
    except TracecatNotFoundError as e:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND, detail="Secret not found"
        ) from e
    return SecretRead.from_database(secret)


@router.post("", status_code=status.HTTP_201_CREATED)
@require_scope("secret:create")
async def create_secret(
    *,
    role: WorkspaceActorRouteRole,
    session: AsyncDBSession,
    params: SecretCreate,
) -> None:
    """Create a secret."""
    service = SecretsService(session, role=role)
    try:
        await service.create_secret(params)
    except ValueError as e:
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(e)
        ) from e
    except IntegrityError as e:
        logger.error("Secret integrity error", e=str(e))
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail="Secret creation integrity error: {e!r}",
        ) from e


@router.post("/{secret_id}", status_code=status.HTTP_204_NO_CONTENT)
@require_scope("secret:update")
async def update_secret_by_id(
    *,
    role: WorkspaceActorRouteRole,
    session: AsyncDBSession,
    secret_id: AnySecretIDPath,
    params: SecretUpdate,
) -> None:
    """Update a secret by ID."""
    service = SecretsService(session, role)
    try:
        secret = await service.get_secret(secret_id)
        await service.update_secret(secret, params)
    except ValueError as e:
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(e)
        ) from e
    except TracecatNotFoundError as e:
        logger.error("Secret not found", secret_id=secret_id)
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND, detail="Secret does not exist"
        ) from e
    except IntegrityError as e:
        logger.info("Secret already exists", secret_id=secret_id)
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT, detail="Secret already exists"
        ) from e


@router.delete("/{secret_id}", status_code=status.HTTP_204_NO_CONTENT)
@require_scope("secret:delete")
async def delete_secret_by_id(
    *,
    role: WorkspaceActorRouteRole,
    session: AsyncDBSession,
    secret_id: AnySecretIDPath,
) -> None:
    """Delete a secret by ID."""
    service = SecretsService(session, role=role)
    try:
        secret = await service.get_secret(secret_id)
        await service.delete_secret(secret)
    except TracecatNotFoundError as e:
        logger.info(f"Secret {secret_id=} not found")
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND, detail="Secret does not exist"
        ) from e


@org_router.get("")
@require_scope("org:secret:read")
async def list_org_secrets(
    *,
    role: OrgActorRole,
    session: AsyncDBSession,
    types: set[SecretType] | None = Query(
        None, alias="type", description="Filter by secret type"
    ),
) -> list[SecretReadMinimal]:
    """List organization secrets."""
    service = SecretsService(session, role=role)
    secrets = await service.list_org_secrets(types=types)
    return [
        _serialize_secret_read_minimal(service=service, secret=secret)
        for secret in secrets
    ]


@org_router.get("/{secret_name}")
@require_scope("org:secret:read")
async def get_org_secret_by_name(
    *,
    role: OrgActorRole,
    session: AsyncDBSession,
    secret_name: str,
    environment: str | None = Query(None),
) -> OrganizationSecretRead:
    """Get an organization secret by name."""
    service = SecretsService(session, role=role)
    try:
        secret = await service.get_org_secret_by_name(secret_name, environment)
    except TracecatNotFoundError as e:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Organization secret not found",
        ) from e
    return OrganizationSecretRead.from_database(secret)


@org_router.post("", status_code=status.HTTP_201_CREATED)
@require_scope("org:secret:create")
async def create_org_secret(
    *,
    role: OrgActorRole,
    session: AsyncDBSession,
    params: SecretCreate,
) -> None:
    """Create an organization secret."""
    service = SecretsService(session, role=role)
    try:
        await service.create_org_secret(params)
    except ValueError as e:
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(e)
        ) from e
    except IntegrityError as e:
        logger.error("Organization secret integrity error", e=str(e))
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=f"Organization secret creation integrity error: {e!r}",
        ) from e


@org_router.post(
    "/{secret_id}",
    status_code=status.HTTP_204_NO_CONTENT,
)
@require_scope("org:secret:update")
async def update_org_secret_by_id(
    *,
    role: OrgActorRole,
    session: AsyncDBSession,
    secret_id: AnySecretIDPath,
    params: SecretUpdate,
) -> None:
    """Update an organization secret by ID."""
    service = SecretsService(session, role)
    try:
        secret = await service.get_org_secret(secret_id)
        await service.update_org_secret(secret, params)
    except ValueError as e:
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(e)
        ) from e
    except TracecatNotFoundError as e:
        logger.error("Organization secret not found", secret_id=secret_id)
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Organization secret does not exist",
        ) from e
    except IntegrityError as e:
        logger.info("Organization secret already exists", secret_id=secret_id)
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail="Organization secret already exists",
        ) from e


@org_router.delete(
    "/{secret_id}",
    status_code=status.HTTP_204_NO_CONTENT,
)
@require_scope("org:secret:delete")
async def delete_org_secret_by_id(
    *,
    role: OrgActorRole,
    session: AsyncDBSession,
    secret_id: AnySecretIDPath,
) -> None:
    """Delete an organization secret by ID."""
    service = SecretsService(session, role=role)
    try:
        secret = await service.get_org_secret(secret_id)
        await service.delete_org_secret(secret)
    except TracecatNotFoundError as e:
        logger.info(f"Organization secret {secret_id=} not found")
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Organization secret does not exist",
        ) from e
