#!/usr/bin/env python
"""Backfill / bootstrap Cloudflare two-plane hosting for an existing project.

From repo root (same ``.env`` as API + worker):

  # RECOMMENDED: full two-plane backfill — creates preview branch in GitHub,
  # seeds production + preview deploy rows, enqueues both workers.
  python -m scripts.bootstrap_project_hosting full-backfill <project_uuid>

  # Production-only bootstrap (idempotent — skips if bootstrap deploy already exists)
  python -m scripts.bootstrap_project_hosting bootstrap <project_uuid>

  # Re-queue an existing deploy row (e.g. failed run) — needs a running arq worker
  python -m scripts.bootstrap_project_hosting retry-deploy <deploy_uuid>

Requires: DATABASE_URL, REDIS_URL, Cloudflare settings (CLOUDFLARE_API_TOKEN,
CLOUDFLARE_ACCOUNT_ID, etc.) and GitHub App settings.
"""
from __future__ import annotations

import argparse
import asyncio
import os
import sys
from pathlib import Path
from uuid import UUID

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from src.core.orm_import import import_all_orm_models

import_all_orm_models()


async def cmd_bootstrap(project_id: str) -> int:
    from src.core.database import async_session_factory
    from src.domain.projects.repository import ProjectRepository, ProjectRepoRepository
    from src.domain.revisions.repository import RevisionRepository
    from src.workers.project_setup_pipeline import enqueue_initial_production_hosting

    pid = UUID(project_id)
    async with async_session_factory() as db:
        proj = await ProjectRepository(db).get_by_id(pid)
        if not proj:
            print(f"Project not found: {project_id}", file=sys.stderr)
            return 1
        if proj.deleted_at is not None:
            print("Project is deleted.", file=sys.stderr)
            return 1
        pr = await ProjectRepoRepository(db).get_by_project_id(pid)
        if not pr or not pr.repo_full_name:
            print("No project_repos row or repo_full_name — link GitHub first.", file=sys.stderr)
            return 1
        rev_repo = RevisionRepository(db)
        rev = await rev_repo.get_latest(pid)
        if not rev:
            print("No revision for project — run project setup or create a revision first.", file=sys.stderr)
            return 1
        repo_full = pr.repo_full_name
        rev_id = rev.id
        await db.commit()

    print(f"Project {pid} repo={repo_full} revision={rev_id}")
    await enqueue_initial_production_hosting(async_session_factory, pid, rev_id, repo_full)
    print(
        "If a new deploy was created, ``run_project_deploy`` was enqueued. "
        "Confirm the arq worker is running and check logs / project_deploys row."
    )
    return 0


async def cmd_retry_deploy(deploy_id: str) -> int:
    from src.workers.enqueue import enqueue_run_project_deploy

    did = UUID(deploy_id)
    ok = await enqueue_run_project_deploy(str(did))
    if ok:
        print(f"Enqueued run_project_deploy for {did}")
        return 0
    print(
        "Enqueue returned False — check Redis matches the worker and logs (arq_enqueue_skipped).",
        file=sys.stderr,
    )
    return 2


async def _force_enqueue_production_hosting(
    db_factory,
    project_id: UUID,
    revision_id: UUID,
    repo_full_name: str,
) -> None:
    """Like enqueue_initial_production_hosting but always creates a new deploy row."""
    from src.domain.deployments.repository import DeployRepository
    from src.workers.enqueue import enqueue_run_project_deploy

    import structlog
    log = structlog.get_logger()

    new_deploy_id = None
    async with db_factory() as db:
        d = await DeployRepository(db).create(
            project_id,
            revision_id,
            "production",
            created_by=None,
            deployment_metadata={
                "bootstrap_initial_hosting": True,
                "github_repo_full_name": repo_full_name,
            },
        )
        new_deploy_id = d.id
        await db.commit()

    ok = await enqueue_run_project_deploy(str(new_deploy_id))
    log.info("hosting_bootstrap_force_enqueued", project_id=str(project_id), deploy_id=str(new_deploy_id), arq_enqueued=ok)
    print(f"  Created deploy row {new_deploy_id} — enqueued={ok}")


async def cmd_full_backfill(project_id: str, preview_branch: str, force: bool = False) -> int:
    """Full two-plane backfill:
    1. Ensure preview branch exists in GitHub (create from main SHA if absent)
    2. Update project_repos.preview_branch in DB
    3. Enqueue production bootstrap (CF Pages project + W4P routing)
    4. Seed preview row + enqueue provision_preview_for_revision
    """
    import httpx

    from src.config import get_settings
    from src.core.database import async_session_factory
    from src.domain.previews.repository import PreviewRepository
    from src.domain.hosting.project_provider import W4P_STATIC
    from src.domain.projects.repository import ProjectRepository, ProjectRepoRepository
    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
    from src.workers.enqueue import enqueue_provision_preview_for_revision
    from src.workers.project_setup_pipeline import enqueue_initial_production_hosting
    from src.workers.repo_ops import (
        ensure_git_remote_push,
        get_project_repo_path,
        inject_deploy_workflow,
        run_git as _git_run,
    )

    settings = get_settings()
    pid = UUID(project_id)

    # --- Load project + repo state ---
    async with async_session_factory() as db:
        proj = await ProjectRepository(db).get_by_id(pid)
        if not proj:
            print(f"Project not found: {project_id}", file=sys.stderr)
            return 1
        if proj.deleted_at is not None:
            print("Project is deleted.", file=sys.stderr)
            return 1

        pr = await ProjectRepoRepository(db).get_by_project_id(pid)
        if not pr or not pr.repo_full_name:
            print("No project_repos row — run project setup first.", file=sys.stderr)
            return 1

        rev = await RevisionRepository(db).get_latest(pid)
        if not rev:
            print("No revision for project — run project setup first.", file=sys.stderr)
            return 1

        workspace_id = proj.workspace_id
        repo_full = pr.repo_full_name
        default_branch = pr.default_branch or "main"
        project_type = proj.type
        project_ref = getattr(proj, "project_ref", None) or ""
        rev_id = rev.id
        rev_sha = rev.git_sha
        # This command is an explicit migration/backfill operation. Persist
        # the decision before workflow generation so both main and preview
        # receive the W4P workflow and all subsequent workers agree.
        proj.primary_hosting_provider = W4P_STATIC
        await db.commit()

    print(f"Project  : {pid}")
    print(f"Repo     : {repo_full}")
    print(f"Type     : {project_type}  ref={project_ref or '(none)'}")
    print(f"Revision : {rev_id}  sha={rev_sha[:12] if rev_sha else 'n/a'}")
    print(f"Branches : {default_branch} (main)  →  {preview_branch} (preview)")
    print()

    # --- Resolve GitHub client (needed for secrets + branch ops) ---
    installation_id = await resolve_installation_id_for_workspace(async_session_factory, workspace_id)
    github = GitHubAppClient(installation_id=installation_id)

    # --- Set Cloudflare secrets on the repo ---
    cf_token = settings.cloudflare_api_token.strip()
    cf_account = settings.cloudflare_account_id.strip()
    if cf_token and cf_account:
        print("Setting CLOUDFLARE_* secrets on repo ...")
        try:
            await github.set_repo_action_secrets(
                repo_full,
                {"CLOUDFLARE_API_TOKEN": cf_token, "CLOUDFLARE_ACCOUNT_ID": cf_account},
            )
            print("  Secrets set.")
        except Exception as exc:
            print(
                f"  WARNING: could not set secrets: {exc}\n"
                "  Add 'Secrets: Read and write' to the GitHub App and re-approve the installation,\n"
                "  then re-run this script.",
                file=sys.stderr,
            )
    else:
        print("  WARNING: CLOUDFLARE_API_TOKEN or CLOUDFLARE_ACCOUNT_ID not in .env — skipping secrets.")
    print()

    # --- Inject / update GitHub Actions deploy workflow ---
    repo_path = get_project_repo_path(settings.repo_workspace, workspace_id, pid)
    if project_ref and os.path.isdir(os.path.join(repo_path, ".git")):
        print("Injecting GitHub Actions deploy workflow ...")
        changed = await inject_deploy_workflow(
            repo_path, project_type, project_ref,
            git_name=settings.git_committer_name,
            git_email=settings.git_committer_email,
            project_created_at=proj.created_at,
            settings=settings,
            primary_hosting_provider=getattr(proj, "primary_hosting_provider", None),
            assume_unassigned_w4p=True,
        )

        # Push if: (a) local commit is new, OR (b) remote repo doesn't have the workflow file yet
        needs_push = changed
        if not needs_push:
            # Check whether deploy.yml actually exists on the remote
            try:
                headers = await github._headers()  # noqa: SLF001
                async with httpx.AsyncClient(timeout=15) as hc:
                    r = await hc.get(
                        f"https://api.github.com/repos/{repo_full}/contents/.github/workflows/deploy.yml",
                        headers=headers,
                    )
                needs_push = (r.status_code == 404)
                if needs_push:
                    print("  deploy.yml missing on remote — will push")
            except Exception as exc:
                print(f"  WARNING: could not check remote workflow status: {exc}", file=sys.stderr)

        auth_url = await github.get_authenticated_clone_url(repo_full)

        if needs_push:
            print("  Pushing main branch with deploy workflow ...")
            await ensure_git_remote_push(repo_path, auth_url, default_branch)
            print("  main branch pushed with deploy workflow")
        else:
            print("  deploy.yml already on remote — no push needed")

        # --- Also ensure preview branch has the workflow ---
        print("Checking deploy.yml on preview branch ...")
        try:
            headers = await github._headers()  # noqa: SLF001
            async with httpx.AsyncClient(timeout=15) as hc:
                r = await hc.get(
                    f"https://api.github.com/repos/{repo_full}/contents/.github/workflows/deploy.yml",
                    headers=headers,
                    params={"ref": preview_branch},
                )
            preview_needs_push = (r.status_code == 404)
        except Exception as exc:
            print(f"  WARNING: could not check preview branch workflow status: {exc}", file=sys.stderr)
            preview_needs_push = False

        if preview_needs_push:
            print(f"  deploy.yml missing on '{preview_branch}' — cherry-picking onto preview ...")
            # Find the workflow commit SHA on main
            _, wf_sha, _ = await _git_run(repo_path, "log", "--format=%H", "-1", "--", ".github/workflows/deploy.yml")
            wf_sha = (wf_sha or "").strip()
            if not wf_sha:
                print("  WARNING: could not find workflow commit SHA — push deploy.yml to preview manually.", file=sys.stderr)
            else:
                # Fetch so remote preview branch is available locally
                await _git_run(repo_path, "fetch", "origin")
                # Checkout preview (creates local tracking branch from origin/preview)
                rc_co, _, err_co = await _git_run(repo_path, "checkout", "-B", preview_branch, f"origin/{preview_branch}")
                if rc_co != 0:
                    print(f"  WARNING: could not checkout '{preview_branch}': {err_co}", file=sys.stderr)
                else:
                    rc_cp, _, err_cp = await _git_run(repo_path, "cherry-pick", wf_sha)
                    if rc_cp != 0:
                        # Cherry-pick failed (e.g. conflict or already applied) — abort and skip
                        await _git_run(repo_path, "cherry-pick", "--abort")
                        print(f"  WARNING: cherry-pick failed ({err_cp.strip()}) — skipping preview workflow push.", file=sys.stderr)
                    else:
                        await ensure_git_remote_push(repo_path, auth_url, preview_branch)
                        print(f"  deploy.yml pushed to '{preview_branch}'")
                # Return to main
                await _git_run(repo_path, "checkout", default_branch)
        else:
            print(f"  deploy.yml already on '{preview_branch}'")
    else:
        print(
            f"  WARNING: local workspace not found at {repo_path} or project_ref missing.\n"
            "  Add .github/workflows/deploy.yml to the repo manually (see docs)."
        )
    print()

    # --- Ensure preview branch exists in GitHub ---

    main_sha = await github.get_ref_head_sha(repo_full, default_branch)
    if not main_sha:
        print(f"Cannot read SHA for {default_branch} on {repo_full}. Check GitHub App permissions.", file=sys.stderr)
        return 1

    preview_sha = await github.get_ref_head_sha(repo_full, preview_branch)
    if preview_sha:
        print(f"Preview branch '{preview_branch}' already exists at {preview_sha[:12]}")
    else:
        print(f"Creating preview branch '{preview_branch}' from {default_branch} @ {main_sha[:12]} ...")
        headers = await github._headers()  # noqa: SLF001
        async with httpx.AsyncClient() as client:
            resp = await client.post(
                f"https://api.github.com/repos/{repo_full}/git/refs",
                headers=headers,
                json={"ref": f"refs/heads/{preview_branch}", "sha": main_sha},
            )
            if resp.status_code not in (201, 422):  # 422 = already exists race
                resp.raise_for_status()
        print(f"  Created refs/heads/{preview_branch}")

    # --- Persist preview_branch to DB ---
    async with async_session_factory() as db:
        await ProjectRepoRepository(db).update_remote_metadata(pid, preview_branch=preview_branch)
        await db.commit()
    print(f"Updated project_repos.preview_branch = '{preview_branch}'")

    # --- Production bootstrap (CF Pages project + W4P routing) ---
    print()
    print("Enqueuing production bootstrap (run_project_deploy) ...")
    if force:
        print("  --force: creating new deploy row (bypassing already_bootstrapped guard) ...")
        await _force_enqueue_production_hosting(async_session_factory, pid, rev_id, repo_full)
    else:
        await enqueue_initial_production_hosting(async_session_factory, pid, rev_id, repo_full)

    # --- Preview row + enqueue provision_preview_for_revision ---
    print("Seeding preview row + enqueuing provision_preview_for_revision ...")
    async with async_session_factory() as db:
        preview_repo = PreviewRepository(db)
        pe = await preview_repo.get_latest_for_revision(pid, rev_id)
        if not pe:
            pe = await preview_repo.create(pid, rev_id, status="provisioning")
            await db.commit()
            print(f"  Created PreviewEnvironment {pe.id}")
        else:
            print(f"  Reusing existing PreviewEnvironment {pe.id} (status={pe.status})")
            if pe.status in ("failed", "error", "stopped"):
                await preview_repo.update_status(pe.id, "provisioning")
                await db.commit()
        preview_id = pe.id

    await enqueue_provision_preview_for_revision(str(preview_id), str(pid), str(rev_id))

    print()
    print("Done. Both jobs have been enqueued.")
    print("Start the arq worker if not already running:")
    print("  arq src.workers.main.WorkerSettings")
    print()
    print(f"Production URL : https://proj-{getattr(proj, 'project_ref', '<ref>')}.{settings.hosting_public_apex}/")
    print(f"Preview URL    : https://preview.proj-{getattr(proj, 'project_ref', '<ref>')}.{settings.hosting_public_apex}/")
    return 0


def main() -> int:
    p = argparse.ArgumentParser(description="Bootstrap or retry Cloudflare two-plane hosting")
    sub = p.add_subparsers(dest="cmd", required=True)

    fb = sub.add_parser(
        "full-backfill",
        help="Full two-plane backfill: preview branch + production + preview deploy (recommended)",
    )
    fb.add_argument("project_id", help="Project UUID")
    fb.add_argument(
        "--preview-branch",
        default="preview",
        help="Preview branch name (default: preview)",
    )
    fb.add_argument(
        "--force",
        action="store_true",
        help="Force new production deploy row even if already_bootstrapped",
    )

    b = sub.add_parser("bootstrap", help="Production-only bootstrap (CF Pages + W4P, idempotent)")
    b.add_argument("project_id", help="Project UUID")

    r = sub.add_parser("retry-deploy", help="Enqueue run_project_deploy for an existing deploy row")
    r.add_argument("deploy_id", help="project_deploys.id UUID")

    args = p.parse_args()
    if args.cmd == "full-backfill":
        return asyncio.run(cmd_full_backfill(args.project_id, args.preview_branch, force=args.force))
    if args.cmd == "bootstrap":
        return asyncio.run(cmd_bootstrap(args.project_id))
    if args.cmd == "retry-deploy":
        return asyncio.run(cmd_retry_deploy(args.deploy_id))
    return 1


if __name__ == "__main__":
    raise SystemExit(main())
