"""Mark codegen runs stuck in ``running`` as failed so clients stop showing an endless spinner.

Workers can die, ARQ can time out, or the Cursor CLI can hang in ways that never update the row.
The main task also reclaims stale ``running`` jobs on retry; this cron covers the no-retry case.
"""
from __future__ import annotations

from datetime import datetime, timedelta, timezone

import structlog
from sqlalchemy import and_, or_, select

from src.config import get_settings

logger = structlog.get_logger()


async def sweep_stale_codegen_runs(ctx: dict) -> dict:
    from src.core.database import async_session_factory
    from src.core.pubsub import get_pubsub_redis
    from src.core.run_events import publish_project_event_dual
    from src.domain.builder.repository import TimelineEventRepository
    from src.domain.codegen.models import CodegenRun
    from src.domain.codegen.repository import CodegenRunRepository

    settings = get_settings()
    db_factory = ctx.get("db_factory", async_session_factory)
    now = datetime.now(timezone.utc)
    threshold = now - timedelta(seconds=settings.codegen_stale_running_seconds)

    async with db_factory() as db:
        # ``started_at`` is normally set when status→running; guard NULL (legacy/bug) via ``created_at``.
        result = await db.execute(
            select(CodegenRun).where(
                CodegenRun.status == "running",
                or_(
                    and_(CodegenRun.started_at.is_not(None), CodegenRun.started_at < threshold),
                    and_(CodegenRun.started_at.is_(None), CodegenRun.created_at < threshold),
                ),
            )
        )
        stale = list(result.scalars().all())
        if not stale:
            return {"marked_failed": 0}

        redis = await get_pubsub_redis()
        try:
            codegen_repo = CodegenRunRepository(db)
            timeline_repo = TimelineEventRepository(db)
            msg = (
                "Build timed out or the worker stopped. "
                "Use Retry or send a new prompt."
            )
            for run in stale:
                await codegen_repo.update_status(
                    run.id,
                    "failed",
                    error_message=msg,
                )
                await timeline_repo.create(
                    run.project_id,
                    "run_failed",
                    run_id=run.id,
                    phase="error",
                    title="Build timed out",
                    detail_text=msg,
                )
                await publish_project_event_dual(
                    redis,
                    str(run.project_id),
                    "codegen.failed",
                    {"run_id": str(run.id), "error": msg},
                )
            await db.commit()
        finally:
            await redis.aclose()

    logger.info("codegen_stale_sweep_done", marked_failed=len(stale))
    return {"marked_failed": len(stale)}
