"""GitHub webhook event handlers.

Each handler processes a specific GitHub event type and
updates P2HS state accordingly (revisions, PR tracking, builds, etc.).
"""
from __future__ import annotations
import structlog
from sqlalchemy.ext.asyncio import AsyncSession

from src.domain.revisions.repository import RevisionRepository

logger = structlog.get_logger()

# Workflow names injected by generate_deploy_workflow() / generate_w4p_deploy_workflow()
_CODEGEN_WORKFLOW_NAME = "P2HS Codegen"


async def _get_project_repo(db: AsyncSession, repo_full_name: str):
    from sqlalchemy import select
    from src.domain.projects.models import ProjectRepo
    result = await db.execute(
        select(ProjectRepo).where(ProjectRepo.repo_full_name == repo_full_name)
    )
    return result.scalar_one_or_none()


async def _get_active_actions_codegen_run(db: AsyncSession, project_id):
    from sqlalchemy import select
    from src.domain.codegen.models import CodegenRun

    result = await db.execute(
        select(CodegenRun)
        .where(CodegenRun.project_id == project_id)
        .where(CodegenRun.status.in_(("running", "preview_refreshing")))
        .where(CodegenRun.worker_checkpoint.in_(("github_actions_dispatched", "github_actions_continuing", "github_actions_deploying")))
        .order_by(CodegenRun.created_at.desc())
        .limit(1)
    )
    return result.scalar_one_or_none()


async def _publish_codegen_workflow_progress(
    *,
    project_id,
    run_id,
    text: str,
    stream: str = "stdout",
) -> None:
    from src.core.pubsub import get_pubsub_redis
    from src.core.run_events import publish_project_event_dual

    redis = await get_pubsub_redis()
    try:
        await publish_project_event_dual(
            redis,
            str(project_id),
            "codegen.ai_output",
            {
                "run_id": str(run_id),
                "text": text,
                "type": "output",
                "stream": stream,
                "stage": "p2hs_ai",
            },
        )
    finally:
        await redis.aclose()


async def _finalize_successful_actions_codegen_from_webhook(
    db: AsyncSession,
    *,
    project_id,
    repo_full_name: str,
    branch: str,
    run_id,
) -> bool:
    """Recover a successful Actions run when its direct completion callback was lost."""
    from sqlalchemy import select

    from src.core.database import async_session_factory
    from src.domain.codegen.models import CodegenRun
    from src.domain.codegen.repository import CodegenRunRepository
    from src.domain.projects.models import Project
    from src.integrations.github_client import GitHubAppClient
    from src.integrations.github_installation import resolve_installation_id_for_workspace
    from src.workers.enqueue import enqueue_codegen_run

    run = (await db.execute(select(CodegenRun).where(CodegenRun.id == run_id))).scalar_one_or_none()
    if run and run.worker_checkpoint == "github_actions_deploying":
        # The workflow-level success arrives after the deploy job. Recover the exact
        # saved commit and declarations, never whatever HEAD happens to be now.
        saved = dict(run.checkpoint_payload or {})
        if not saved.get("commit_sha"):
            return False
        repo = CodegenRunRepository(db)
        await repo.update_status(run.id, "running")
        await repo.update_checkpoint(run.id, "pushed", saved)
        await db.commit()
        await enqueue_codegen_run(str(run.id), project_id=str(project_id))
        return True

    project = (await db.execute(select(Project).where(Project.id == project_id))).scalar_one_or_none()
    if not project:
        return False
    installation_id = await resolve_installation_id_for_workspace(
        async_session_factory, project.workspace_id
    )
    github = GitHubAppClient(installation_id=installation_id)
    commit_sha = await github.get_ref_head_sha(repo_full_name, branch)
    if not commit_sha:
        logger.warning(
            "codegen_workflow_completion_missing_branch_head",
            project_id=str(project_id), run_id=str(run_id), branch=branch,
        )
        return False

    run = (await db.execute(select(CodegenRun).where(CodegenRun.id == run_id))).scalar_one_or_none()
    if not run or run.status != "running" or run.worker_checkpoint != "github_actions_dispatched":
        return False
    await CodegenRunRepository(db).update_checkpoint(
        run.id,
        "pushed",
        {
            "commit_sha": commit_sha,
            "summary": "P2HS AI workflow completed. Finalizing your app.",
            "provider": "github_actions",
        },
    )
    await db.commit()
    await enqueue_codegen_run(str(run.id), project_id=str(project_id))
    logger.info(
        "codegen_workflow_completion_reconciled",
        project_id=str(project_id), run_id=str(run.id), commit_sha=commit_sha,
    )
    return True


async def _handle_template_push(db: AsyncSession, repo_full_name: str, branch: str, sha: str) -> dict | None:
    """If ``repo_full_name`` is a template repo, handle CI/CD for template branches.

    Returns a result dict if this push was for a template (caller should return it),
    or None if the repo is not a template repo.
    """
    from src.domain.templates.repository import TemplateRepository
    from src.workers.enqueue import enqueue_template_deploy

    repo = TemplateRepository(db)
    templates = await repo.get_by_github_repo(repo_full_name)
    if not templates:
        return None

    results = []
    for template in templates:
        if branch == template.starter_branch:
            # Update starter_sha — next user project from this template gets the latest boilerplate.
            await repo.update(template.id, starter_sha=sha)
            await db.commit()
            logger.info(
                "template_starter_sha_updated",
                template_id=str(template.id),
                ref=template.ref,
                sha=sha[:12],
            )
            results.append({"template_id": str(template.id), "action": "starter_sha_updated"})

        elif branch == template.main_branch:
            await db.commit()  # flush any starter_sha update
            try:
                ok = await enqueue_template_deploy(str(template.id), environment="production")
                logger.info(
                    "template_main_push_deploy_enqueued",
                    template_id=str(template.id),
                    ref=template.ref,
                    sha=sha[:12],
                    enqueued=ok,
                )
                results.append({"template_id": str(template.id), "action": "deploy_enqueued", "environment": "production"})
            except Exception as exc:
                logger.warning("template_deploy_enqueue_failed", template_id=str(template.id), error=str(exc))

        elif branch == template.dev_branch:
            await db.commit()
            try:
                ok = await enqueue_template_deploy(str(template.id), environment="development")
                logger.info(
                    "template_dev_push_deploy_enqueued",
                    template_id=str(template.id),
                    ref=template.ref,
                    sha=sha[:12],
                    enqueued=ok,
                )
                results.append({"template_id": str(template.id), "action": "deploy_enqueued", "environment": "development"})
            except Exception as exc:
                logger.warning("template_deploy_enqueue_failed", template_id=str(template.id), error=str(exc))

    if results:
        return {"action": "template_push_handled", "results": results}
    return None


async def handle_push(db: AsyncSession, payload: dict) -> dict:
    """Handle push event: create a new platform revision if the push
    came from a P2HS-managed repo on the default branch.

    Also handles template repo pushes: triggers demo redeployment on main/dev pushes
    and updates starter_sha on starter branch pushes.

    Pushes to the preview branch are ignored here — previews are driven
    by codegen runs, not raw git events.
    """
    ref = payload.get("ref", "")
    head_commit = payload.get("head_commit", {})
    repo_full_name = payload.get("repository", {}).get("full_name", "")
    sha = head_commit.get("id", "")
    message = head_commit.get("message", "")
    branch = ref.replace("refs/heads/", "")

    # Check for template repo pushes first (before user project checks).
    template_result = await _handle_template_push(db, repo_full_name, branch, sha)
    if template_result is not None:
        return template_result

    default_branch = payload.get("repository", {}).get("default_branch", "main")
    if branch != default_branch:
        logger.info("github_push_non_default_branch", repo=repo_full_name, branch=branch)
        return {"action": "skipped", "reason": "not_default_branch"}

    project_repo = await _get_project_repo(db, repo_full_name)
    if not project_repo:
        logger.info("github_push_unknown_repo", repo=repo_full_name)
        return {"action": "skipped", "reason": "repo_not_managed"}

    # Skip internal P2HS pushes (codegen, merge, workflow injection) to avoid duplicate revisions
    if message.startswith("chore:") or message.startswith("[p2hs]"):
        logger.info("github_push_internal_commit_skipped", repo=repo_full_name, sha=sha[:8])
        return {"action": "skipped", "reason": "internal_commit"}

    revision_repo = RevisionRepository(db)
    existing = await revision_repo.get_by_git_sha(project_repo.project_id, sha)
    if existing:
        return {"action": "skipped", "reason": "sha_already_tracked"}

    rev = await revision_repo.create(
        project_id=project_repo.project_id,
        git_sha=sha,
        source="github_push",
        commit_message=message,
    )
    await db.commit()

    logger.info("github_push_revision_created", repo=repo_full_name, sha=sha[:8], revision_id=str(rev.id))
    return {"action": "revision_created", "revision_id": str(rev.id), "sha": sha}


async def handle_pull_request(db: AsyncSession, payload: dict) -> dict:
    """Handle pull_request event: track PR status for codegen runs."""
    action = payload.get("action", "")
    pr = payload.get("pull_request", {})
    repo_full_name = payload.get("repository", {}).get("full_name", "")
    pr_number = pr.get("number")
    merged = pr.get("merged", False)
    head_sha = pr.get("head", {}).get("sha", "")

    logger.info("github_pr_event", repo=repo_full_name, action=action, pr=pr_number, merged=merged)

    if action == "closed" and merged:
        project_repo = await _get_project_repo(db, repo_full_name)
        if project_repo:
            merge_sha = pr.get("merge_commit_sha", head_sha)
            revision_repo = RevisionRepository(db)
            existing = await revision_repo.get_by_git_sha(project_repo.project_id, merge_sha)
            if not existing:
                rev = await revision_repo.create(
                    project_id=project_repo.project_id,
                    git_sha=merge_sha,
                    source="github_pr_merge",
                    commit_message=f"PR #{pr_number} merged: {pr.get('title', '')}",
                )
                await db.commit()
                return {"action": "revision_created_from_merge", "revision_id": str(rev.id)}

    return {"action": "tracked", "pr_action": action}


async def handle_workflow_run(db: AsyncSession, payload: dict) -> dict:
    """Handle workflow_run completed: trigger preview/production routing after a
    successful CF Pages deploy workflow run.

    This replaces the arq retry-polling loop in provision_preview_for_revision —
    wrangler pages deploy is synchronous, so when the workflow finishes the CF Pages
    deployment is already indexed.
    """
    action = payload.get("action", "")
    workflow_run = payload.get("workflow_run", {})
    conclusion = workflow_run.get("conclusion")  # success | failure | cancelled | ...
    status = workflow_run.get("status")
    workflow_name = workflow_run.get("name", "")
    head_sha = workflow_run.get("head_sha", "")
    head_branch = workflow_run.get("head_branch", "")
    repo_full_name = payload.get("repository", {}).get("full_name", "")
    sha_short = head_sha[:12] if head_sha else ""

    logger.info(
        "github_workflow_run",
        repo=repo_full_name, action=action, status=status, conclusion=conclusion,
        workflow=workflow_name, branch=head_branch, sha=sha_short,
    )

    if action != "completed":
        return {"action": "workflow_in_progress", "status": status}

    if _CODEGEN_WORKFLOW_NAME in workflow_name:
        project_repo = await _get_project_repo(db, repo_full_name)
        if not project_repo:
            return {"action": "skipped", "reason": "repo_not_managed"}
        run = await _get_active_actions_codegen_run(db, project_repo.project_id)
        if not run:
            return {"action": "skipped", "reason": "codegen_run_not_found"}
        payload = run.checkpoint_payload or {}
        active_head = str(payload.get("dispatch_commit_sha") or "")
        if active_head and head_sha and active_head != head_sha:
            logger.info(
                "codegen_workflow_stale_attempt_ignored",
                project_id=str(project_repo.project_id),
                run_id=str(run.id),
                workflow_head=sha_short,
                active_head=active_head[:12],
                conclusion=conclusion,
            )
            return {"action": "skipped", "reason": "stale_workflow_attempt"}
        if run.worker_checkpoint == "github_actions_continuing":
            return {"action": "skipped", "reason": "continuation_in_progress"}
        if conclusion == "success":
            reconciled = await _finalize_successful_actions_codegen_from_webhook(
                db,
                project_id=project_repo.project_id,
                repo_full_name=repo_full_name,
                branch=head_branch,
                run_id=run.id,
            )
            await _publish_codegen_workflow_progress(
                project_id=project_repo.project_id,
                run_id=run.id,
                text="P2HS AI finished. Finalizing your app.",
            )
            return {
                "action": "codegen_workflow_completed",
                "conclusion": conclusion,
                "reconciled": reconciled,
            }

        from src.domain.codegen.repository import CodegenRunRepository

        message = f"P2HS AI workflow {conclusion or 'failed'} before completion callback."
        await CodegenRunRepository(db).update_status(
            run.id,
            "failed" if conclusion != "cancelled" else "canceled",
            error_message=message,
        )
        await db.commit()
        await _publish_codegen_workflow_progress(
            project_id=project_repo.project_id,
            run_id=run.id,
            text=message,
            stream="stderr",
        )
        return {"action": "codegen_workflow_failed", "conclusion": conclusion}

    from src.domain.hosting.deploy_trigger import is_generated_app_deploy_workflow

    if not is_generated_app_deploy_workflow(workflow_name):
        return {"action": "skipped", "reason": "not_deploy_workflow", "workflow": workflow_name}

    project_repo = await _get_project_repo(db, repo_full_name)
    if not project_repo:
        return {"action": "skipped", "reason": "repo_not_managed"}

    project_id = project_repo.project_id
    preview_branch = getattr(project_repo, "preview_branch", None) or "preview"
    default_branch = getattr(project_repo, "default_branch", None) or "main"

    if conclusion != "success":
        logger.warning(
            "deploy_workflow_failed",
            repo=repo_full_name, branch=head_branch, sha=sha_short, conclusion=conclusion,
        )
        # Mark in-flight preview as failed so the UI doesn't hang on "provisioning"
        if head_branch == preview_branch:
            from src.domain.previews.repository import PreviewRepository
            rev = await RevisionRepository(db).get_by_git_sha(project_id, head_sha)
            if rev:
                pe = await PreviewRepository(db).get_latest_for_revision(project_id, rev.id)
                if pe and pe.status == "provisioning":
                    await PreviewRepository(db).update_status(pe.id, "failed")
                    await db.commit()

        # Attempt auto-fix — non-fatal, failures just get logged
        try:
            await _auto_fix_failed_workflow(
                db=db,
                project_id=project_id,
                project_repo=project_repo,
                workflow_run=workflow_run,
                head_branch=head_branch,
            )
        except Exception as fix_err:
            logger.warning("auto_fix_attempt_error", error=str(fix_err), repo=repo_full_name)

        return {"action": "workflow_failed", "conclusion": conclusion, "branch": head_branch}

    # --- success path ---
    revision_repo = RevisionRepository(db)
    rev = await revision_repo.get_by_git_sha(project_id, head_sha)
    if not rev and head_branch == preview_branch:
        # The workflow may have been triggered by a chore: fix commit (e.g., fix_deploy_workflows
        # updating deploy.yml). The code is identical to the last codegen push, so fall back to
        # the latest codegen revision and create a new revision record for the actual SHA that
        # GA deployed — this ensures run_project_deploy polls CF Pages for the right SHA.
        fallback = await revision_repo.get_by_source(project_id, "codegen")
        if fallback:
            rev = await revision_repo.create(
                project_id=project_id,
                git_sha=head_sha,
                source="codegen",
                source_id=fallback.source_id,
                commit_message=f"[p2hs] Redeployed at {sha_short} (workflow fix)",
            )
            await db.commit()
            logger.info(
                "workflow_run_created_revision_for_sha",
                repo=repo_full_name, sha=sha_short, revision_id=str(rev.id),
            )
    if not rev:
        logger.warning("workflow_run_no_revision", repo=repo_full_name, sha=sha_short, branch=head_branch)
        return {"action": "skipped", "reason": "revision_not_found", "sha": sha_short}

    if head_branch == preview_branch:
        # Gate: only provision preview for codegen revisions, not the boilerplate push.
        if rev.source != "codegen":
            logger.info("workflow_run_preview_gate", sha=sha_short, source=rev.source)
            return {"action": "skipped", "reason": "boilerplate_gate"}

        # Preview deploy finished — enqueue routing setup (no CF Pages polling needed)
        from src.domain.previews.repository import PreviewRepository
        from src.workers.enqueue import enqueue_provision_preview_for_revision

        preview_repo = PreviewRepository(db)
        pe = await preview_repo.get_latest_for_revision(project_id, rev.id)
        if not pe:
            pe = await preview_repo.create(project_id, rev.id, status="provisioning")
            await db.commit()
        elif pe.status == "running":
            logger.info("workflow_run_preview_already_running", preview_id=str(pe.id))
            return {"action": "skipped", "reason": "preview_already_running"}
        elif pe.status in ("failed", "error", "stopped"):
            await preview_repo.update_status(pe.id, "provisioning")
            await db.commit()

        ok = await enqueue_provision_preview_for_revision(
            str(pe.id), str(project_id), str(rev.id), webhook_triggered=True
        )
        logger.info("workflow_run_preview_enqueued", preview_id=str(pe.id), sha=sha_short, enqueued=ok)
        return {"action": "preview_enqueued", "preview_id": str(pe.id), "sha": sha_short}

    if head_branch == default_branch:
        # Production deploy finished — create a deploy row and enqueue run_project_deploy.
        # This covers the case where the polling task exhausted retries before GH Actions finished.
        from src.domain.deployments.repository import DeployRepository
        from src.workers.enqueue import enqueue_run_project_deploy

        deploy_repo = DeployRepository(db)
        existing = await deploy_repo.find_for_revision_environment(
            project_id, rev.id, "production"
        )
        if existing is not None:
            logger.info(
                "workflow_run_production_reused",
                deploy_id=str(existing.id),
                status=existing.status,
                sha=sha_short,
            )
            return {
                "action": "production_deploy_reused",
                "deploy_id": str(existing.id),
                "sha": sha_short,
            }

        async with db.begin_nested():
            d = await deploy_repo.create(
                project_id,
                rev.id,
                "production",
                created_by=None,
                deployment_metadata={
                    "bootstrap_initial_hosting": True,
                    "github_repo_full_name": repo_full_name,
                    "triggered_by": "workflow_run_webhook",
                },
            )
        await db.commit()
        ok = await enqueue_run_project_deploy(str(d.id))
        logger.info("workflow_run_production_enqueued", deploy_id=str(d.id), sha=sha_short, enqueued=ok)
        return {"action": "production_deploy_enqueued", "deploy_id": str(d.id), "sha": sha_short}

    return {"action": "skipped", "reason": "branch_not_tracked", "branch": head_branch}


def _find_failed_step(jobs: list[dict]) -> tuple[str | None, int | None]:
    """Return (failed_step_name, failed_job_id) from a list of workflow jobs.

    Walks each job's steps looking for the first step with conclusion='failure'.
    Returns (None, None) if no failed step is found.
    """
    for job in jobs:
        if job.get("conclusion") != "failure":
            continue
        job_id = job.get("id")
        for step in job.get("steps", []):
            if step.get("conclusion") == "failure":
                return step.get("name", ""), job_id
        # job failed but no step marked failed — return the job name as fallback
        return job.get("name", ""), job_id
    return None, None


_BUILD_STEP_KEYWORDS = ("npm", "install", "build", "generate", "compile", "test", "codegen")
# backend_guard.py's own exact exception text (validate_backend_repo / validate_port_artifacts) --
# not a single shared prefix across both check families, so match each distinctly.
_CONTRACT_VIOLATION_MARKERS = (
    "Cloud persistence contract failed",
    "Platform port lock was changed or removed",
    "Platform port artifact missing or unsafe",
    "Platform port artifact modified",
    "Homemade route shadows an installed platform port",
)


def _is_build_step(step_name_lower: str) -> bool:
    return any(kw in step_name_lower for kw in _BUILD_STEP_KEYWORDS)


def _is_contract_violation(log_text: str) -> bool:
    return any(marker in log_text for marker in _CONTRACT_VIOLATION_MARKERS)
_AUTO_FIX_COOLDOWN_HOURS = 6
_AUTO_FIX_MAX_RECENT = 3


async def _auto_fix_failed_workflow(
    db: AsyncSession,
    project_id,
    project_repo,
    workflow_run: dict,
    head_branch: str,
) -> None:
    """Analyze a failed deploy workflow run and attempt the appropriate automatic fix.

    Two fix paths:
    A. Workflow file outdated (wrangler/action version issue) → enqueue fix_deploy_workflows
    B. Build step failed (npm install / npm run generate) → create AI auto_fix codegen run
    """
    from datetime import datetime, timezone, timedelta

    from sqlalchemy import func as sql_func, select

    from src.domain.codegen.models import CodegenRun
    from src.domain.projects.models import Project
    from src.integrations.github_client import GitHubAppClient
    from src.integrations.github_installation import resolve_installation_id_for_workspace
    from src.workers.enqueue import enqueue_codegen_run, enqueue_fix_deploy_workflows
    from src.core.database import async_session_factory

    run_id = workflow_run.get("id")
    repo_full_name = project_repo.repo_full_name
    if not run_id:
        return

    # ── Cooldown: don't spam if we already triggered several auto-fixes recently ──
    cutoff = datetime.now(timezone.utc) - timedelta(hours=_AUTO_FIX_COOLDOWN_HOURS)
    count_result = await db.execute(
        select(sql_func.count()).where(
            CodegenRun.project_id == project_id,
            CodegenRun.run_mode == "auto_fix",
            CodegenRun.created_at >= cutoff,
        )
    )
    recent = count_result.scalar() or 0
    if recent >= _AUTO_FIX_MAX_RECENT:
        logger.info(
            "auto_fix_cooldown",
            project_id=str(project_id), recent=recent,
            max=_AUTO_FIX_MAX_RECENT, window_hours=_AUTO_FIX_COOLDOWN_HOURS,
        )
        return

    # ── Get the project for workspace_id + type ──────────────────────────────
    proj_result = await db.execute(select(Project).where(Project.id == project_id))
    project = proj_result.scalar_one_or_none()
    if not project:
        return
    workspace_id = project.workspace_id
    project_type = project.type or "html"

    # ── Fetch workflow run jobs to identify which step failed ─────────────────
    installation_id = await resolve_installation_id_for_workspace(async_session_factory, workspace_id)
    github = GitHubAppClient(installation_id=installation_id)
    jobs = await github.get_workflow_run_jobs(repo_full_name, run_id)
    if not jobs:
        # Can't determine step — run the workflow fix as a safe default
        await enqueue_fix_deploy_workflows(str(project_id))
        return

    failed_step_name, failed_job_id = _find_failed_step(jobs)
    if not failed_step_name:
        await enqueue_fix_deploy_workflows(str(project_id))
        return

    step_lower = failed_step_name.lower()
    logger.info(
        "auto_fix_failed_step",
        project_id=str(project_id), repo=repo_full_name,
        step=failed_step_name, job_id=failed_job_id,
    )

    # ── Path A: Deploy/wrangler step failed → update deploy.yml ──────────────
    is_deploy_step = (
        "deploy" in step_lower
        or "wrangler" in step_lower
        or "cloudflare" in step_lower
        or "pages" in step_lower
    )
    if is_deploy_step:
        logger.info("auto_fix_dispatch_workflow_fix", project_id=str(project_id), step=failed_step_name)
        await enqueue_fix_deploy_workflows(str(project_id))
        return

    # ── Path B: Build step failed → ask AI to fix the code ───────────────────
    is_build_step = _is_build_step(step_lower)
    if not is_build_step:
        logger.info("auto_fix_unrecognized_step", project_id=str(project_id), step=failed_step_name)
        return

    if not failed_job_id:
        return

    log_text = await github.get_job_log_text(repo_full_name, failed_job_id)
    if not log_text:
        logger.warning("auto_fix_no_log_text", project_id=str(project_id), job_id=failed_job_id)
        return

    # Build the auto_fix codegen run
    from src.domain.codegen.prompt_wrapper import build_system_prompt
    from src.domain.codegen.repository import CodegenRunRepository

    try:
        instruction_content = ""
        try:
            from src.domain.instructions.repository import InstructionRepository
            async with async_session_factory() as fresh_db:
                instruction_content = await InstructionRepository(fresh_db).get_current_content(project.id)
        except Exception:
            pass

        is_contract_violation = _is_contract_violation(log_text)

        if is_contract_violation:
            system_prompt = (
                build_system_prompt(
                    project_type=project_type,
                    instruction_content=instruction_content,
                )
                + "\n\n## Task: Fix a Platform Contract Violation\n"
                "The platform's own pre-push contract check rejected this run's changes — "
                "it is not a build error. The violations below each name the file and the "
                "specific rule it broke. Fix each one by using the platform's own installed "
                "adapter/capability for that concern (see PlatformBackendCapabilities."
                "runtime_contract and p2hs/app-recipe.json in this repo) — do not silence, "
                "catch, or delete the check, and do not reimplement the concern a different "
                "way that would still violate the same rule."
            )
            user_prompt = (
                f"The platform contract check failed on branch `{head_branch}`.\n\n"
                f"Violations:\n\n```\n{log_text}\n```"
            )
        else:
            system_prompt = (
                build_system_prompt(
                    project_type=project_type,
                    instruction_content=instruction_content,
                )
                + "\n\n## Task: Fix GitHub Actions Build Failure\n"
                "A GitHub Actions step failed during the automated deployment. "
                "Your task is to fix the code so the build passes. "
                "Make only the minimal changes required — do not refactor or rewrite unrelated code."
            )
            user_prompt = (
                f"The GitHub Actions step **\"{failed_step_name}\"** failed on branch `{head_branch}`.\n\n"
                f"Fix the error shown in the following log:\n\n```\n{log_text}\n```"
            )

        codegen_repo = CodegenRunRepository(db)
        run = await codegen_repo.create(
            project_id=project_id,
            created_by=None,
            run_mode="auto_fix",
        )
        await codegen_repo.update_status(
            run.id,
            "queued",
            plan={
                "system_prompt": system_prompt,
                "user_prompt": user_prompt,
                "original_user_prompt": f"Auto-fix: {failed_step_name} failed on {head_branch}",
                "context": {
                    "project_type": project_type,
                    "run_mode": "auto_fix",
                    "failed_step": failed_step_name,
                    "branch": head_branch,
                    "auto_fix_reason": "contract_violation" if is_contract_violation else "build_failure",
                },
            },
        )
        await db.commit()
        await enqueue_codegen_run(str(run.id))
        if is_contract_violation:
            logger.info(
                "codegen.contract_violation",
                project_id=str(project_id), run_id=str(run.id),
                step=failed_step_name, branch=head_branch,
            )
        logger.info(
            "auto_fix_codegen_run_created",
            project_id=str(project_id), run_id=str(run.id),
            step=failed_step_name, branch=head_branch,
            reason="contract_violation" if is_contract_violation else "build_failure",
        )
    except Exception as exc:
        logger.error("auto_fix_codegen_run_failed", project_id=str(project_id), error=str(exc))


async def handle_workflow_job(db: AsyncSession, payload: dict) -> dict:
    """Handle workflow_job events: log job-level progress for observability."""
    action = payload.get("action", "")  # queued | in_progress | completed | waiting
    job = payload.get("workflow_job", {})
    job_name = job.get("name", "")
    conclusion = job.get("conclusion")
    repo_full_name = payload.get("repository", {}).get("full_name", "")

    logger.info(
        "github_workflow_job",
        repo=repo_full_name, action=action, job=job_name, conclusion=conclusion,
    )
    workflow_name = job.get("workflow_name", "")
    if _CODEGEN_WORKFLOW_NAME in workflow_name or job_name == "codegen":
        project_repo = await _get_project_repo(db, repo_full_name)
        if project_repo:
            run = await _get_active_actions_codegen_run(db, project_repo.project_id)
            if run:
                if action in ("queued", "waiting"):
                    text = "Deploy Agent is queued."
                elif action == "in_progress":
                    text = "Deploy Agent is running."
                elif action == "completed":
                    text = (
                        "Deploy Agent completed."
                        if conclusion == "success"
                        else f"Deploy Agent {conclusion or 'completed'}."
                    )
                else:
                    text = f"Deploy Agent {action}."
                await _publish_codegen_workflow_progress(
                    project_id=project_repo.project_id,
                    run_id=run.id,
                    text=text,
                    stream="stderr" if conclusion and conclusion != "success" else "stdout",
                )
    return {"action": "job_tracked", "job_action": action, "job": job_name}


async def handle_installation(db: AsyncSession, payload: dict) -> dict:
    """Handle installation event: track GitHub App installs/uninstalls."""
    action = payload.get("action", "")
    installation_id = payload.get("installation", {}).get("id")
    account = payload.get("installation", {}).get("account", {}).get("login", "")

    logger.info("github_installation", action=action, installation_id=installation_id, account=account)
    return {"action": action, "installation_id": installation_id}


EVENT_HANDLERS = {
    "push": handle_push,
    "pull_request": handle_pull_request,
    "workflow_run": handle_workflow_run,
    "workflow_job": handle_workflow_job,
    "installation": handle_installation,
    "installation_repositories": handle_installation,
}
