"""Delete expired run work branches (p2hs/codegen/<run_id>), see domain/codegen/work_branch.py.

Failed and blocked branches are kept for investigation and retry for
``codegen_work_branch_retention_days``; a promoted branch is only history (preview already has
the work), so it goes much sooner. A branch is deleted only if Core's own stored state names it
for that run, and the GitHub client refuses to delete anything that is not a run branch.
"""

from __future__ import annotations

from datetime import UTC, datetime

import structlog
from sqlalchemy import select

logger = structlog.get_logger()

PROMOTED_RETENTION_DAYS = 3
_PAGE = 100
_MAX_PAGES = 10
_TERMINAL = ["failed", "blocked", "promoted"]


async def cleanup_codegen_work_branches(ctx: dict) -> dict:
    from src.config import get_settings
    from src.core.database import async_session_factory
    from src.domain.codegen.models import CodegenRun
    from src.domain.codegen.work_branch import is_expired, persist_state, valid_state
    from src.domain.projects.repository import ProjectRepository
    from src.integrations.github_client import GitHubAppClient
    from src.integrations.github_installation import resolve_installation_id_for_workspace

    settings = get_settings()
    results = {"checked": 0, "deleted": 0, "skipped": 0, "errors": 0}
    if not settings.codegen_work_branch_enabled:
        return {**results, "status": "disabled"}
    db_factory = ctx.get("db_factory") or async_session_factory
    now = datetime.now(UTC)

    # Read the bounded sweep set before mutating any rows.  Offset pagination
    # over terminal rows is unsafe here: changing a processed row to
    # ``deleted`` removes it from the next SELECT and makes the next page skip
    # rows that were never examined.
    candidates = []
    for page in range(_MAX_PAGES):
        async with db_factory() as db:
            rows = (
                await db.execute(
                    select(CodegenRun.id, CodegenRun.project_id, CodegenRun.plan)
                    .where(CodegenRun.plan["context"]["work_branch"]["status"].astext.in_(_TERMINAL))
                    .order_by(CodegenRun.created_at.asc())
                    .offset(page * _PAGE)
                    .limit(_PAGE)
                )
            ).all()
        if not rows:
            break
        candidates.extend(rows)
        if len(rows) < _PAGE:
            break
    for run_id, project_id, plan in candidates:
        results["checked"] += 1
        state = ((plan or {}).get("context") or {}).get("work_branch")
        if not valid_state(state, run_id):
            results["skipped"] += 1
            continue
        days = (
            PROMOTED_RETENTION_DAYS
            if state.get("status") == "promoted"
            else int(settings.codegen_work_branch_retention_days)
        )
        if not is_expired(state, now, days):
            results["skipped"] += 1
            continue
        try:
            async with db_factory() as db:
                project = await ProjectRepository(db).get_with_repo(project_id)
                repo = getattr(project, "repo", None) if project else None
                repo_full_name = getattr(repo, "repo_full_name", None)
                workspace_id = getattr(project, "workspace_id", None)
            if repo_full_name and workspace_id:
                installation_id = await resolve_installation_id_for_workspace(
                    db_factory, workspace_id
                )
                await GitHubAppClient(installation_id=installation_id).delete_work_branch(
                    repo_full_name, state["branch"]
                )
            # No repo any more means there is nothing left to delete: just record it.
            await persist_state(
                db_factory,
                run_id,
                {**state, "status": "deleted", "updated_at": now.isoformat()},
            )
            results["deleted"] += 1
            logger.info(
                "work_branch_deleted",
                run_id=str(run_id),
                branch=state["branch"],
                was=state.get("status"),
            )
        except Exception as exc:  # one bad branch must not stop the sweep
            results["errors"] += 1
            logger.warning(
                "work_branch_delete_failed", run_id=str(run_id), error_type=type(exc).__name__
            )
    return results
