"""Agent management and OAuth endpoints. Policy is owned by the service."""

import time
from uuid import UUID

import structlog
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import JSONResponse
from pydantic import ValidationError as SchemaValidationError

from src.core.exceptions import P2HSError, UnauthorizedError
from src.domain.agent_access.dependencies import (
    get_agent_principal,
    get_billing_delegation_service,
    get_agent_service,
    require_agent_access,
)
from src.domain.agent_access.billing_delegation import Decision
from src.domain.agent_access.forms import read_oauth_form
from src.domain.agent_access.rate_limit import TOOL_RATE_CLASS
from src.domain.agent_access.schemas import (
    AuthorizationRequest,
    ClientCreate,
    ConsentCreate,
    GrantCreate,
    GrantUpdate,
    ToolCall,
)
from src.domain.agent_access.service import AgentAccessService, fingerprint
from src.domain.agent_access.tools import AgentToolService
from src.domain.auth.router import get_current_user

logger = structlog.get_logger()

# Argument keys that are safe to log as an access-trail target: entity identifiers,
# never free-text content (build prompts), amounts, or provider/payment details.
_LOGGABLE_TARGET_KEYS = ("workspace_id", "project_id", "run_id", "request_key")

router = APIRouter(
    prefix="/api/agent-access", tags=["agent-access"], dependencies=[Depends(require_agent_access)]
)


def _target_fields(arguments: dict) -> dict:
    if not isinstance(arguments, dict):
        return {}
    return {
        key: str(arguments[key]) for key in _LOGGABLE_TARGET_KEYS if arguments.get(key) is not None
    }


@router.post("/clients", status_code=201)
async def register_client(body: ClientCreate, request: Request, service=Depends(get_agent_service)):
    # DCR is public. Rate limit before creating durable rows; fail closed if Redis is unavailable.
    from src.core.redis import get_redis

    # The route guard authenticates the dashboard before trusting its forwarded client IP.
    key = "agent-client-registration:" + fingerprint(
        request.headers.get("CF-Connecting-IP")
        or (request.client.host if request.client else "unknown")
    )
    try:
        async for redis in get_redis():
            count = await redis.eval(
                "local n=redis.call('INCR',KEYS[1]); "
                "if n==1 then redis.call('EXPIRE',KEYS[1],3600) end; return n",
                1,
                key,
            )
            if count > 30:
                raise HTTPException(429, "Too many client registrations")
            return await service.register_client(body)
    except HTTPException:
        raise
    except Exception:
        raise HTTPException(503, "Client registration temporarily unavailable") from None
    raise HTTPException(503, "Client registration temporarily unavailable")


@router.post("/authorization-context")
async def authorization_context(
    body: AuthorizationRequest, user=Depends(get_current_user), service=Depends(get_agent_service)
):
    return await service.authorization_context(body)


@router.post("/consent")
async def consent(
    body: ConsentCreate, user=Depends(get_current_user), service=Depends(get_agent_service)
):
    return await service.approve(body, user.id)


@router.post("/service-accounts", status_code=201)
async def service_account(
    body: GrantCreate, user=Depends(get_current_user), service=Depends(get_agent_service)
):
    return await service.create_service_account(body, user.id)


@router.get("/workspace-options")
async def workspace_options(user=Depends(get_current_user), service=Depends(get_agent_service)):
    return await service.workspace_options(user.id)


@router.get("/grants")
async def my_grants(user=Depends(get_current_user), service=Depends(get_agent_service)):
    return await service.list_my_grants(user.id)


@router.patch("/grants/{grant_id}")
async def update_grant(
    grant_id: UUID,
    body: GrantUpdate,
    user=Depends(get_current_user),
    service=Depends(get_agent_service),
):
    return await service.update_grant(grant_id, body, user.id)


@router.post("/workspaces/{workspace_id}/billing-delegation-requests", status_code=201)
async def request_billing_delegation(
    workspace_id: UUID,
    user=Depends(get_current_user),
    service=Depends(get_billing_delegation_service),
):
    """A member asks the workspace owner to let them put billing scopes on agent connections."""
    return await service.request(workspace_id, user.id)


@router.get("/workspaces/{workspace_id}/billing-delegations")
async def billing_delegations(
    workspace_id: UUID,
    user=Depends(get_current_user),
    service=Depends(get_billing_delegation_service),
):
    return await service.list_for_owner(workspace_id, user.id)


@router.post("/workspaces/{workspace_id}/billing-delegations/{delegation_id}/{decision}")
async def resolve_billing_delegation(
    workspace_id: UUID,
    delegation_id: UUID,
    decision: Decision,
    user=Depends(get_current_user),
    service=Depends(get_billing_delegation_service),
):
    return await service.resolve(workspace_id, delegation_id, user.id, decision)


@router.get("/workspaces/{workspace_id}/grants")
async def grants(
    workspace_id: UUID, user=Depends(get_current_user), service=Depends(get_agent_service)
):
    return await service.list_grants(workspace_id, user.id)


@router.delete("/workspaces/{workspace_id}/grants/{grant_id}")
async def revoke_grant(
    workspace_id: UUID,
    grant_id: UUID,
    user=Depends(get_current_user),
    service=Depends(get_agent_service),
):
    await service.revoke(workspace_id, grant_id, user.id)
    return {"revoked": True}


@router.post("/token")
async def token(request: Request, service: AgentAccessService = Depends(get_agent_service)):
    headers = {"Cache-Control": "no-store", "Pragma": "no-cache"}
    try:
        result = await service.exchange(await read_oauth_form(request))
        return JSONResponse(result, headers=headers)
    except P2HSError as exc:
        error = "invalid_grant" if isinstance(exc, UnauthorizedError) else "invalid_request"
        return JSONResponse({"error": error}, status_code=400, headers=headers)


@router.post("/revoke")
async def revoke_token(request: Request, service: AgentAccessService = Depends(get_agent_service)):
    try:
        await service.revoke_token(await read_oauth_form(request))
    except P2HSError:
        return JSONResponse(
            {"error": "invalid_request"}, status_code=400, headers={"Cache-Control": "no-store"}
        )
    return JSONResponse({}, headers={"Cache-Control": "no-store"})


@router.post("/capabilities")
async def capabilities(principal=Depends(get_agent_principal), service=Depends(get_agent_service)):
    return await AgentToolService(service, service.repo).capabilities(principal)


@router.post("/tools")
async def execute_tool(
    body: ToolCall, principal=Depends(get_agent_principal), service=Depends(get_agent_service)
):
    rate_class = TOOL_RATE_CLASS.get(body.name, "unclassified")
    structlog.contextvars.bind_contextvars(agent_tool=body.name, agent_rate_class=rate_class)
    target = _target_fields(body.arguments)
    started = time.perf_counter()

    def _audit(status: str, **extra) -> None:
        logger.info(
            "agent_tool.executed",
            tool_name=body.name,
            operation_class=rate_class,
            status=status,
            duration_ms=round((time.perf_counter() - started) * 1000, 2),
            **target,
            **extra,
        )

    try:
        result = await AgentToolService(service, service.repo).execute(
            principal, body.name, body.arguments
        )
    except SchemaValidationError:
        _audit("invalid_arguments")
        raise HTTPException(422, "Invalid tool arguments") from None
    except P2HSError as exc:
        _audit("denied", error_type=type(exc).__name__)
        raise
    except Exception as exc:
        _audit("error", error_type=type(exc).__name__)
        raise
    operation_id = result.get("operation_id") if isinstance(result, dict) else None
    _audit("success", **({"operation_id": operation_id} if operation_id else {}))
    return result
