"""Roll the current generated-app base Worker runtime onto existing W4P apps.

This migration deliberately does not regenerate customer application files.  It
refreshes the checked-in W4P workflow (the workflow contains the shared Worker
runtime source), creates an exact-SHA Core deployment record, and dispatches the
project's existing ``deploy.yml``.  Core therefore keeps the normal OIDC,
authorization, routing, and deployment audit gates.

Preview-first by default:

    python scripts/refresh_w4p_base_runtime.py --all-workspaces
    python scripts/refresh_w4p_base_runtime.py --all-workspaces --execute

Use ``--environment production`` only after the preview rollout has been
verified.  The command is idempotent for a project/branch SHA that already has
an active runtime-refresh deployment.
"""
from __future__ import annotations

import argparse
import asyncio
import json
import sys
from pathlib import Path
from typing import Sequence
from uuid import UUID

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

SUPPORTED_TYPES = ("html", "react_app", "nuxt_app")
W4P_PROVIDERS = {"cloudflare_w4p_static", "cloudflare_w4p", "w4p"}
RUNTIME_REFRESH_CONTRACT = "p2hs-generated-app-base-v2"


def _prepare_orm_models() -> None:
    from src.core.orm_import import import_all_orm_models

    import_all_orm_models()


async def _select_projects(
    *, project_ids: Sequence[str] = (), workspace_id: str | None = None
) -> list[dict[str, str]]:
    from sqlalchemy import and_, select
    from src.core.database import async_session_factory
    from src.domain.projects.models import Project, ProjectRepo

    requested = {str(value) for value in project_ids}
    async with async_session_factory() as db:
        stmt = (
            select(Project.id, Project.workspace_id, Project.name, Project.type)
            .join(ProjectRepo, ProjectRepo.project_id == Project.id)
            .where(
                and_(
                    Project.deleted_at.is_(None),
                    Project.status.in_(("ready", "creating")),
                    Project.type.in_(SUPPORTED_TYPES),
                    Project.primary_hosting_provider.in_(W4P_PROVIDERS),
                )
            )
            .order_by(Project.created_at.asc())
        )
        if workspace_id:
            stmt = stmt.where(Project.workspace_id == workspace_id)
        if requested:
            stmt = stmt.where(Project.id.in_(requested))
        rows = (await db.execute(stmt)).all()

    selected = [
        {
            "project_id": str(project_id),
            "workspace_id": str(project_workspace_id),
            "name": str(name or ""),
            "type": str(project_type or ""),
        }
        for project_id, project_workspace_id, name, project_type in rows
    ]
    if requested:
        found = {row["project_id"] for row in selected}
        missing = sorted(requested - found)
        if missing:
            raise ValueError(
                "project not found, unsupported, deleted, or not an explicit W4P project: "
                + ", ".join(missing)
            )
    return selected


async def _prepare_branch(*, project, repo, environment: str, settings, db_factory) -> tuple[str, bool, str]:
    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,
        get_project_repo_path,
        git_rev_parse_head,
        prepare_working_tree,
    )

    branch = (
        getattr(repo, "preview_branch", None) or settings.hosting_preview_branch or "preview"
        if environment == "preview"
        else getattr(repo, "default_branch", None) or "main"
    )
    installation_id = await resolve_installation_id_for_workspace(db_factory, project.workspace_id)
    github = GitHubAppClient(installation_id=installation_id)
    clone_url = await github.get_authenticated_clone_url(repo.repo_full_name)
    repo_path = get_project_repo_path(settings.repo_workspace, project.workspace_id, project.id)
    await prepare_working_tree(
        repo_path,
        clone_url,
        branch=branch,
        heal_missing_remote_from=getattr(repo, "default_branch", None) or "main",
    )
    changed = await ensure_deploy_workflow(
        repo_path,
        project.type or "html",
        project.project_ref or "",
        git_name=settings.git_committer_name,
        git_email=settings.git_committer_email,
        project_created_at=project.created_at,
        settings=settings,
        primary_hosting_provider=project.primary_hosting_provider,
        assume_unassigned_w4p=False,
    )
    sha = await git_rev_parse_head(repo_path)
    if not sha:
        raise RuntimeError(f"no_head_after_runtime_workflow_refresh:{project.id}:{branch}")
    return sha, changed, repo_path


async def _refresh_project(project_id: str, environment: str, db_factory) -> dict:
    from src.config import get_settings
    from src.domain.deployments.repository import DeployRepository
    from src.domain.hosting.ci_activation import WAITING_FOR_CI_STEP
    from src.domain.hosting.project_provider import W4P_STATIC
    from src.domain.projects.repository import ProjectRepository
    from src.domain.revisions.repository import RevisionRepository
    from src.integrations.github_client import GitHubAppClient
    from src.integrations.github_installation import resolve_installation_id_for_workspace

    settings = get_settings()
    async with db_factory() as db:
        project = await ProjectRepository(db).get_with_repo(UUID(project_id))
        if project is None or not getattr(project, "repo", None):
            raise RuntimeError("project_or_repo_missing")
        repo = project.repo
        branch = (
            (getattr(repo, "preview_branch", None) or settings.hosting_preview_branch or "preview")
            if environment == "preview"
            else (getattr(repo, "default_branch", None) or "main")
        )
        installation_id = await resolve_installation_id_for_workspace(
            db_factory, project.workspace_id
        )
        github = GitHubAppClient(installation_id=installation_id)
        sha = await github.get_ref_head_sha(repo.repo_full_name, branch)
        if not sha:
            return {"project_id": project_id, "environment": environment, "status": "skipped", "reason": "branch_missing"}
        deployments = await DeployRepository(db).list_for_project(project.id)
        existing = next(
            (
                d
                for d in deployments
                if d.environment == environment
                and d.source_git_sha == sha
                and (d.deployment_metadata or {}).get("runtime_refresh_contract")
                == RUNTIME_REFRESH_CONTRACT
                and d.status in {"pending", "deploying", "live"}
            ),
            None,
        )
        if existing:
            return {"project_id": project_id, "environment": environment, "status": "skipped", "reason": "already_scheduled", "sha": sha}

    sha, workflow_changed, repo_path = await _prepare_branch(
        project=project, repo=repo, environment=environment, settings=settings, db_factory=db_factory
    )
    async with db_factory() as db:
        revision_repo = RevisionRepository(db)
        revision = await revision_repo.get_by_git_sha(project.id, sha)
        if revision is None:
            revision = await revision_repo.create(
                project.id,
                git_sha=sha,
                source="runtime_refresh",
                commit_message=f"[p2hs] refresh generated app base runtime {sha[:12]}",
            )
        deploy_repo = DeployRepository(db)
        deploy = await deploy_repo.create(
            project.id,
            revision.id,
            environment,
            deployment_metadata={
                "runtime_refresh_contract": RUNTIME_REFRESH_CONTRACT,
                "target_kind": "w4p_static",
                "environment": environment,
            },
        )
        await deploy_repo.apply_hosting_outcome(
            deploy.id,
            status="deploying",
            hosting_provider=W4P_STATIC,
            source_git_sha=sha,
            last_completed_step=WAITING_FOR_CI_STEP,
            metadata_patch={"runtime_refresh_workflow_changed": workflow_changed},
        )
        await db.commit()
        deploy_id = str(deploy.id)

    if workflow_changed:
        from src.workers.repo_ops import push_repo

        await push_repo(repo_path, (
            getattr(repo, "preview_branch", None) or settings.hosting_preview_branch or "preview"
            if environment == "preview"
            else getattr(repo, "default_branch", None) or "main"
        ), force_with_lease=True)

    installation_id = await resolve_installation_id_for_workspace(db_factory, project.workspace_id)
    github = GitHubAppClient(installation_id=installation_id)
    dispatched = workflow_changed or await github.trigger_workflow_dispatch(
        repo_full_name=repo.repo_full_name,
        workflow_file="deploy.yml",
        ref=(
            getattr(repo, "preview_branch", None) or settings.hosting_preview_branch or "preview"
            if environment == "preview"
            else getattr(repo, "default_branch", None) or "main"
        ),
    )
    if not dispatched:
        raise RuntimeError(f"workflow_dispatch_rejected:{project_id}:{environment}")
    return {"project_id": project_id, "environment": environment, "status": "queued", "sha": sha, "deploy_id": deploy_id}


async def _main_async(args: argparse.Namespace) -> tuple[list[dict[str, str]], list[dict]]:
    _prepare_orm_models()
    from src.core.database import async_session_factory

    selected = await _select_projects(project_ids=args.project_ids or (), workspace_id=args.workspace)
    if args.dry_run:
        return selected, [
            {**project, "status": "dry_run", "environments": args.environments}
            for project in selected
        ]
    # Reuse one DB factory for the helper and all project rows.
    results: list[dict] = []
    for project in selected:
        for environment in args.environments:
            try:
                results.append(await _refresh_project(project["project_id"], environment, async_session_factory))
            except Exception as exc:
                results.append({"project_id": project["project_id"], "environment": environment, "status": "failed", "error": str(exc)[:2000]})
    return selected, results


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    selector = parser.add_mutually_exclusive_group(required=True)
    selector.add_argument("--project", action="append", dest="project_ids")
    selector.add_argument("--workspace")
    selector.add_argument("--all-workspaces", action="store_true")
    parser.add_argument("--environment", choices=("preview", "production", "both"), default="preview")
    parser.add_argument("--execute", action="store_true", help="Apply the migration; default is read-only.")
    args = parser.parse_args()
    args.dry_run = not args.execute
    args.environments = ("preview", "production") if args.environment == "both" else (args.environment,)
    selected, result = asyncio.run(_main_async(args))
    print(json.dumps({"selected": len(selected), "projects": result}, indent=2, default=str))
    if any(row.get("status") == "failed" for row in result):
        raise SystemExit(1)


if __name__ == "__main__":
    main()
