"""Recoverable dispatcher for Core agent project/build operations."""

import structlog
from sqlalchemy import select

from src.config import get_settings
from src.core.exceptions import P2HSError
from src.domain.agent_access.dependencies import make_agent_service
from src.domain.agent_access.models import AgentOperation
from src.domain.agent_access.operations import CreateProject, StartBuild
from src.domain.agent_access.repository import AgentRepository
from src.domain.agent_access.service import utcnow
from src.domain.codegen.agent_facade import AgentCodegenFacade
from src.domain.codegen.import_snapshot import summarize
from src.domain.projects.agent_facade import AgentProjectFacade

log = structlog.get_logger()


async def dispatch_agent_operations(ctx):
    if not get_settings().agent_access_enabled:
        return
    # A failed operation must not prevent unrelated tenants' work from progressing.
    async with ctx["db_factory"]() as db:
        ids = list(
            (
                await db.scalars(
                    select(AgentOperation.id)
                    .where(
                        AgentOperation.status.in_(["queued", "dispatch_pending"]),
                        AgentOperation.action.in_(["create_project", "start_build"]),
                    )
                    .order_by(AgentOperation.updated_at)
                    .limit(50)
                )
            ).all()
        )
    for operation_id in ids:
        try:
            async with ctx["db_factory"]() as db:
                async with db.begin():
                    operation = (
                        await db.execute(
                            select(AgentOperation)
                            .where(
                                AgentOperation.id == operation_id,
                                AgentOperation.status.in_(["queued", "dispatch_pending"]),
                            )
                            .with_for_update(skip_locked=True)
                        )
                    ).scalar_one_or_none()
                    if not operation:
                        continue
                    access = await make_agent_service(db)
                    grant = await access.repo.grant(operation.grant_id, lock=True)
                    try:
                        access.require_active(grant)
                        if operation.action == "create_project":
                            await access.require_scope(
                                grant, "projects.create", workspace_id=operation.workspace_id
                            )
                            async with db.begin_nested():
                                body = CreateProject(
                                    request_key=operation.request_key, **operation.arguments
                                )
                                project = await AgentProjectFacade(db, access.workspaces).create(
                                    grant, body, operation.id, operation.workspace_id
                                )
                                access_row = await access.access_row(grant, operation.workspace_id)
                                if not access_row.workspace_wide:
                                    access_row.project_ids = [
                                        *access_row.project_ids,
                                        str(project.id),
                                    ]
                                operation.result = {"project_id": str(project.id)}
                                operation.status = "completed"
                        else:
                            body = StartBuild(
                                request_key=operation.request_key, **operation.arguments
                            )
                            await access.require_scope(grant, "builds.run", body.project_id)
                            if operation.status == "queued":
                                async with db.begin_nested():
                                    run = await AgentCodegenFacade(db).start(
                                        grant, body, prompt_source=operation.prompt_source
                                    )
                                    operation.result = {
                                        "run_id": str(run.id),
                                        "project_id": str(body.project_id),
                                    }
                                    # What Core actually received (paths, sizes, sha256 --
                                    # never contents), so the sender can compare it with
                                    # what it believes it sent.
                                    snapshot = (
                                        ((getattr(run, "plan", None) or {}).get("context") or {})
                                        .get("import_snapshot")
                                    )
                                    if snapshot:
                                        operation.result["import"] = summarize(snapshot)
                                    operation.status = "dispatch_pending"
                            else:
                                # The run is committed before publication. A deterministic arq ID
                                # protects a crash after publication but before this DB commit.
                                await ctx["redis"].enqueue_job(
                                    "run_codegen",
                                    operation.result["run_id"],
                                    _job_id=f"agent-build:{operation.result['run_id']}",
                                )
                                operation.status = "completed"
                    except P2HSError as exc:
                        # The agent only ever sees the generic message below (avoids
                        # leaking cross-tenant/billing detail through the MCP relay) --
                        # but that previously meant Core's own logs gave no way to tell
                        # *which* check failed (permission demotion, unpaid billing,
                        # MFA, not-a-workspace-member, missing project, ...) without
                        # correlating separate policy-layer log lines by workspace_id
                        # and a timestamp guess. Log it here, once, right at the
                        # decision point.
                        log.info(
                            "agent_operation_denied",
                            operation_id=str(operation_id),
                            request_key=operation.request_key,
                            action=operation.action,
                            grant_id=str(operation.grant_id),
                            error_type=type(exc).__name__,
                            error=str(exc),
                        )
                        operation.status = "failed"
                        operation.result = {"error": "Action denied or workspace unavailable"}
                    operation.updated_at = utcnow()
        except Exception:
            # Log only the operation ID; retry durable work next tick.
            log.warning("agent_operation_retry", operation_id=str(operation_id))
            try:
                async with ctx["db_factory"]() as retry_db:
                    async with retry_db.begin():
                        await AgentRepository(retry_db).defer_retry(operation_id, utcnow())
            except Exception:
                pass  # Database outage: durable rows remain available for the next scan.
