"""One-time (and per-project) task: scan active projects and update stale deploy.yml files.

Fixes outdated GitHub Actions versions (Node 20 → 24), missing FORCE_JAVASCRIPT_ACTIONS_TO_NODE24,
npm install → npm ci, and any future generate_deploy_workflow improvements.

Pushes updated deploy.yml to the preview branch for all stale projects.
Does not push main. Production runs only when a user deploys, which merges
preview into main and then creates the production deployment row.

Usage:
  # Scan all projects (one-time migration)
  from src.workers.enqueue import enqueue_fix_deploy_workflows
  await enqueue_fix_deploy_workflows()

  # Fix a single project (triggered by webhook on failure)
  await enqueue_fix_deploy_workflows(project_id="<uuid>")
"""
from __future__ import annotations

import structlog

logger = structlog.get_logger()

_MAX_PER_RUN = 100


async def fix_deploy_workflows(
    ctx: dict,
    project_id: str | None = None,
    sync_runtime_adapters: bool = False,
) -> dict:
    """Scan projects and push updated deploy.yml where the file is stale.

    Args:
        project_id: If provided, only fix that one project. Otherwise scans all active projects.

    Returns dict with counts: checked, updated_preview, updated_main, skipped, errors.
    """
    from uuid import UUID

    from sqlalchemy import and_, select

    from src.config import get_settings
    from src.core.database import async_session_factory
    from src.domain.hosting.deployment_targets import deploy_workflow_for_project
    from src.domain.deployments.repository import DeployRepository
    from src.domain.projects.models import Project, ProjectRepo
    from src.integrations.github_client import GitHubAppClient
    from src.integrations.github_installation import resolve_installation_id_for_workspace
    from src.workers.repo_ops import (
        ensure_deploy_workflow,
        ensure_generated_runtime_adapters,
        get_project_repo_path,
        prepare_working_tree,
        push_repo,
        write_and_commit_file,
    )
    from src.workers.enqueue import enqueue_merge_and_deploy

    db_factory = ctx.get("db_factory") or async_session_factory
    settings = get_settings()
    results: dict[str, int] = {
        "checked": 0,
        "updated_preview": 0,
        "updated_main": 0,
        "updated_runtime_preview": 0,
        "updated_runtime_main": 0,
        "triggered_preview": 0,
        "triggered_main": 0,
        "skipped": 0,
        "errors": 0,
    }

    # ── Load project list ────────────────────────────────────────────────────
    async with db_factory() as db:
        stmt = (
            select(Project, ProjectRepo)
            .join(ProjectRepo, Project.id == ProjectRepo.project_id)
            .where(
                and_(
                    Project.status.in_(["ready", "creating"]),
                    Project.deleted_at.is_(None),
                    ProjectRepo.repo_full_name.isnot(None),
                )
            )
        )
        if project_id:
            stmt = stmt.where(Project.id == UUID(project_id))
        stmt = stmt.limit(_MAX_PER_RUN)
        rows = list((await db.execute(stmt)).all())

    logger.info("fix_deploy_workflows_start", total=len(rows), single_project=bool(project_id))

    for project, repo in rows:
        pid = project.id
        workspace_id = project.workspace_id
        project_type = project.type or "html"
        project_ref = (getattr(project, "project_ref", None) or "").strip()
        repo_full_name = repo.repo_full_name
        preview_branch = getattr(repo, "preview_branch", None) or "preview"
        default_branch = getattr(repo, "default_branch", None) or "main"

        if not project_ref:
            logger.warning("fix_deploy_no_project_ref", project_id=str(pid))
            results["skipped"] += 1
            continue

        try:
            async with db_factory() as _db:
                deploy_history = await DeployRepository(_db).list_for_project(pid)
            has_successful_pages_deployment = any(
                d.status == "live"
                and (d.hosting_provider or "").strip().lower() in {"cloudflare_pages", "cloudflare"}
                for d in deploy_history
            )
            migrate_unprovisioned = not has_successful_pages_deployment
            expected = deploy_workflow_for_project(
                project_type,
                project_ref,
                project.created_at,
                settings,
                primary_hosting_provider=getattr(project, "primary_hosting_provider", None),
                assume_unassigned_w4p=migrate_unprovisioned,
                has_successful_pages_deployment=has_successful_pages_deployment,
            )

            installation_id = await resolve_installation_id_for_workspace(db_factory, workspace_id)
            github = GitHubAppClient(installation_id=installation_id)
            clone_url = await github.get_authenticated_clone_url(repo_full_name)
            repo_path = get_project_repo_path(settings.repo_workspace, workspace_id, pid)

            results["checked"] += 1
            promote_after_preview = False

            # ── Ensure CF secrets are set on the repo ───────────────────────────
            secrets_set = False
            cf_token = (settings.cloudflare_api_token or "").strip()
            cf_account = (settings.cloudflare_account_id or "").strip()
            async with db_factory() as _db:
                from src.domain.cloud_infra.repository import (
                    CFAccountRepository,
                    CloudInstanceRepository,
                    WorkspaceCloudAssignmentRepository,
                )
                from src.domain.cloud_infra.service import CloudInfraService
                from src.core.exceptions import NotFoundError

                try:
                    _svc = CloudInfraService(
                        cf_account_repo=CFAccountRepository(_db),
                        instance_repo=CloudInstanceRepository(_db),
                        assignment_repo=WorkspaceCloudAssignmentRepository(_db),
                    )
                    _creds = await _svc.get_credentials_for_workspace(workspace_id)
                    cf_token = _creds.api_token
                    cf_account = _creds.account_id
                except (NotFoundError, ValueError):
                    pass  # fall back to settings env vars

            if cf_token and cf_account:
                try:
                    await github.set_repo_action_secrets(
                        repo_full_name,
                        {"CLOUDFLARE_API_TOKEN": cf_token, "CLOUDFLARE_ACCOUNT_ID": cf_account},
                    )
                    logger.info("fix_deploy_secrets_set", project_id=str(pid), repo=repo_full_name)
                    secrets_set = True
                except Exception as sec_exc:
                    logger.warning(
                        "fix_deploy_secrets_failed",
                        project_id=str(pid),
                        repo=repo_full_name,
                        error=str(sec_exc),
                    )

            # ── Preview branch ───────────────────────────────────────────────
            try:
                preview_current = (await github.get_file_contents(
                    repo_full_name, ".github/workflows/deploy.yml", preview_branch
                )).decode(errors="replace")
            except Exception:
                preview_current = ""
            try:
                await github.get_file_contents(
                    repo_full_name, "p2hs/platform-ports.lock.json", preview_branch
                )
                preview_lock_missing = False
            except Exception:
                preview_lock_missing = True

            if preview_current.strip() != expected.strip() or preview_lock_missing:
                logger.info("fix_deploy_preview_stale", project_id=str(pid), repo=repo_full_name)
                await prepare_working_tree(
                    repo_path,
                    clone_url,
                    branch=preview_branch,
                    heal_missing_remote_from=default_branch,
                )
                runtime_updated = False
                if sync_runtime_adapters:
                    runtime_updated = await ensure_generated_runtime_adapters(
                        repo_path,
                        project_type,
                        git_name=settings.git_committer_name,
                        git_email=settings.git_committer_email,
                    )
                committed = await ensure_deploy_workflow(
                    repo_path, project_type, project_ref,
                    git_name=settings.git_committer_name,
                    git_email=settings.git_committer_email,
                    project_created_at=project.created_at,
                    settings=settings,
                    primary_hosting_provider=getattr(project, "primary_hosting_provider", None),
                    assume_unassigned_w4p=migrate_unprovisioned,
                    has_successful_pages_deployment=has_successful_pages_deployment,
                )
                if runtime_updated:
                    results["updated_runtime_preview"] += 1
                if committed or runtime_updated:
                    await push_repo(repo_path, preview_branch, force_with_lease=True)
                    if committed:
                        results["updated_preview"] += 1
                    if sync_runtime_adapters:
                        promote_after_preview = True
                    logger.info("fix_deploy_preview_updated", project_id=str(pid), repo=repo_full_name)
            elif secrets_set or sync_runtime_adapters:
                # Workflow file is current but secrets were just configured — push a neutral
                # trigger commit so GitHub Actions re-runs with the secrets now in place.
                await prepare_working_tree(
                    repo_path,
                    clone_url,
                    branch=preview_branch,
                    heal_missing_remote_from=default_branch,
                )
                runtime_updated = False
                if sync_runtime_adapters:
                    runtime_updated = await ensure_generated_runtime_adapters(
                        repo_path,
                        project_type,
                        git_name=settings.git_committer_name,
                        git_email=settings.git_committer_email,
                    )
                triggered = False
                if secrets_set and not runtime_updated:
                    triggered = await write_and_commit_file(
                        repo_path,
                        ".p2hs/deployment-notes.md",
                        "Deployment configuration updated.\n",
                        "[p2hs] Update deployment configuration",
                        git_name=settings.git_committer_name,
                        git_email=settings.git_committer_email,
                    )
                if runtime_updated:
                    results["updated_runtime_preview"] += 1
                if triggered or runtime_updated:
                    await push_repo(repo_path, preview_branch, force_with_lease=True)
                    if triggered:
                        results["triggered_preview"] += 1
                    if sync_runtime_adapters:
                        promote_after_preview = True
                    logger.info("fix_deploy_preview_triggered", project_id=str(pid), repo=repo_full_name)

            # ── Main branch ──────────────────────────────────────────────────
            if sync_runtime_adapters:
                # Runtime repair is preview-first. migrate_runtime_primitives
                # already pushed the named port contract to preview, even when
                # this pass finds the workflow and records adapters current.
                # Promotion still has to run, or production main keeps the
                # default-export contract the W4P bundler rejects.
                promoted = await enqueue_merge_and_deploy(str(pid))
                logger.info(
                    "fix_deploy_production_promotion_enqueued",
                    project_id=str(pid),
                    enqueued=promoted,
                    preview_changed=promote_after_preview,
                )
                continue

            # Main is not updated here. Pushing main starts the production
            # workflow, and that workflow can only activate a deployment row
            # created by Deploy. Stale main files are carried over when preview
            # is merged into main.

        except Exception as exc:
            logger.error(
                "fix_deploy_project_error",
                project_id=str(pid),
                repo=repo_full_name,
                error=str(exc),
                exc_info=True,
            )
            results["errors"] += 1

    logger.info("fix_deploy_workflows_done", **results)
    return results
