"""One-time (and per-project) task: cut leftover generated apps onto current primitives.

Existing HTML/React/Nuxt sites still have homemade contact POSTs, staff inboxes,
notify-to-email, and client-side magic-link consume. New codegen runs declare
``contact-capture`` in the repo and then block at resolveCapabilitiesBeforePush
if Core never installed that pack.

This worker clones preview, installs catalog ports, rewrites leftover app paths,
installs contact-capture in Core, and only then pushes. Later prompts keep the
strict guard and always-on capabilities gate.

Usage:
  from src.workers.enqueue import enqueue_migrate_runtime_primitives
  await enqueue_migrate_runtime_primitives()
  await enqueue_migrate_runtime_primitives(project_id="<uuid>")
"""
from __future__ import annotations

import asyncio
import os
from uuid import UUID

import structlog
from sqlalchemy import select

logger = structlog.get_logger()

_MAX_PER_RUN = 100
_COMMIT_MSG = "[p2hs] migrate leftover apps onto platform contact and consume"


async def migrate_runtime_primitives(ctx: dict, project_id: str | None = None) -> dict:
    from sqlalchemy import and_

    from src.config import get_settings
    from src.core.database import async_session_factory
    from src.domain.cloud.facades import project_port_context
    from src.domain.codegen.app_sdk import ensure_contact_capture_manifest
    from src.domain.codegen.leftover_contact import (
        contact_capture_config_from_repo,
        scan_leftover_notify_emails,
    )
    from src.domain.codegen.port_adapter import install_catalog_adapters
    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 get_project_repo_path, prepare_working_tree, push_repo

    db_factory = ctx.get("db_factory") or async_session_factory
    settings = get_settings()
    results = {"checked": 0, "updated": 0, "skipped": 0, "errors": 0, "pack_blocked": 0}

    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("migrate_runtime_primitives_start", total=len(rows), single_project=bool(project_id))

    for project, repo in rows:
        pid = project.id
        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"
        project_type = project.type or "html"
        results["checked"] += 1
        try:
            async with db_factory() as db:
                live = await db.get(Project, pid)
                if live is None:
                    results["skipped"] += 1
                    continue
                context = await project_port_context(db, live)
                context["legacy_contact_submit"] = True
                from src.domain.hosting.migration_state import artifact_provider_id

                context["hosting_provider"] = artifact_provider_id(
                    getattr(live, "primary_hosting_provider", None),
                    getattr(live, "hosting_migration", None),
                )
                context["project_id"] = str(pid)
                await _log_existing_pack_blocker(db, pid)

            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_full_name)
            repo_path = get_project_repo_path(settings.repo_workspace, project.workspace_id, pid)
            await prepare_working_tree(
                repo_path,
                clone_url,
                branch=preview_branch,
                heal_missing_remote_from=default_branch,
            )
            composition_source = await _write_project_composition(
                db_factory, pid, repo_path
            )
            install_catalog_adapters(repo_path, project_type, context)
            notify = scan_leftover_notify_emails(repo_path)
            ensure_contact_capture_manifest(repo_path, notify_to_email=notify)
            pack_config = contact_capture_config_from_repo(repo_path)
            pack_outcome = await _install_contact_capture_pack(
                db_factory, pid, pack_config, leftover_notify=notify
            )
            if not pack_outcome.get("ok"):
                results["pack_blocked"] += 1
                results["errors"] += 1
                logger.error(
                    "migrate_runtime_primitives_pack_blocked",
                    project_id=str(pid),
                    repo=repo_full_name,
                    errors=pack_outcome.get("errors") or ["blocked"],
                    blocker=pack_outcome.get("blocker"),
                )
                continue
            committed = await _commit_all(
                repo_path,
                _COMMIT_MSG,
                git_name=settings.git_committer_name,
                git_email=settings.git_committer_email,
            )
            sha = await _head_sha(repo_path)
            if committed:
                await push_repo(repo_path, preview_branch, force_with_lease=True)
            elif not sha:
                results["skipped"] += 1
                continue
            else:
                logger.info(
                    "migrate_runtime_primitives_pack_ready_no_repo_delta",
                    project_id=str(pid),
                    repo=repo_full_name,
                )
            if committed and sha:
                await _enqueue_preview_for_sha(db_factory, pid, sha)
            results["updated"] += 1
            logger.info(
                "migrate_runtime_primitives_updated",
                project_id=str(pid),
                repo=repo_full_name,
                commit_sha=(sha or "")[:12],
                composition_source=composition_source,
                notify_to_email=bool(pack_config.get("notify_to_email") or notify),
            )
        except Exception as exc:
            logger.error(
                "migrate_runtime_primitives_error",
                project_id=str(pid),
                repo=repo_full_name,
                error=str(exc),
                exc_info=True,
            )
            results["errors"] += 1

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


async def _write_project_composition(db_factory, project_id: UUID, repo_path: str) -> str:
    """Write the Core composition into the app before port adapters are installed.

    A GenerationPlan is materialized in full, including the current dependency
    closure of its selected blocks. Discovery-only projects declare those packs
    in the manifest without replacing a plan that does not exist. The catalog
    adapter step that follows installs the cookie-session client and the record,
    booking, import, and connector routes those packs call.
    """
    from src.domain.capabilities.operator_reconciliation import desired_composition
    from src.domain.codegen.app_sdk import ensure_pack_manifest
    from src.domain.codegen.composition_manifest import materialize_core_composition

    async with db_factory() as db:
        desired = await desired_composition(db, project_id)
    if desired.get("errors"):
        raise RuntimeError("composition_invalid:" + ",".join(desired["errors"]))
    plan = desired.get("generation_plan")
    if isinstance(plan, dict):
        materialize_core_composition(repo_path, plan)
        return "generation_plan"
    for slug in desired.get("feature_packs") or []:
        if isinstance(slug, str) and slug:
            ensure_pack_manifest(repo_path, slug)
    return str(desired.get("source") or "none")


async def _log_existing_pack_blocker(db, project_id: UUID) -> None:
    from src.domain.feature_packs.models import ProjectFeaturePack
    row = (
        await db.execute(
            select(ProjectFeaturePack).where(
                ProjectFeaturePack.project_id == project_id,
                ProjectFeaturePack.pack_slug == "contact-capture",
            )
        )
    ).scalar_one_or_none()
    if row is None:
        logger.info("migrate_runtime_primitives_pack_absent", project_id=str(project_id))
        return
    logger.info(
        "migrate_runtime_primitives_pack_state",
        project_id=str(project_id),
        status=row.status,
        blocker=row.blocker,
    )


async def _install_contact_capture_pack(
    db_factory, project_id: UUID, config: dict, leftover_notify: str = ""
) -> dict:
    from src.domain.connectors.repository import ConnectorRepository
    from src.domain.edge_functions.repository import (
        FunctionLibraryRepository,
        ProjectFunctionRepository,
        ProjectFunctionTokenRepository,
        ProjectFunctionVersionRepository,
        ProjectWebhookEndpointRepository,
        WorkerRuntimeVersionRepository,
    )
    from src.domain.edge_functions.service import EdgeFunctionsService
    from src.domain.feature_packs.service import PackInstallService
    from src.domain.projects.repository import ProjectRepository
    from src.integrations.cloud_control_client import (
        CloudControlClient,
        get_cloud_client_for_workspace,
    )

    async with db_factory() as db:
        project = await ProjectRepository(db).get_by_id(project_id)
        if project:
            cloud_client = await get_cloud_client_for_workspace(project.workspace_id, db)
        else:
            cloud_client = CloudControlClient()
        edge_svc = EdgeFunctionsService(
            library_repo=FunctionLibraryRepository(db),
            fn_repo=ProjectFunctionRepository(db),
            version_repo=ProjectFunctionVersionRepository(db),
            webhook_repo=ProjectWebhookEndpointRepository(db),
            runtime_repo=WorkerRuntimeVersionRepository(db),
            token_repo=ProjectFunctionTokenRepository(db),
            project_repo=ProjectRepository(db),
            connector_repo=ConnectorRepository(db),
            cloud_client=cloud_client,
        )
        pack_svc = PackInstallService(db, edge_svc, cloud_client)
        outcome = await pack_svc.install_pack(
            project_id=project_id,
            pack_slug="contact-capture",
            config=config or {},
            user_prompt=leftover_notify,
        )
        await db.commit()
        return outcome


async def _enqueue_preview_for_sha(db_factory, project_id: UUID, git_sha: str) -> None:
    from src.domain.previews.repository import PreviewRepository
    from src.domain.projects.repository import ProjectRepository
    from src.domain.revisions.repository import RevisionRepository
    from src.workers.enqueue import enqueue_provision_preview_for_revision

    async with db_factory() as db:
        revision_repo = RevisionRepository(db)
        preview_repo = PreviewRepository(db)
        project_repo = ProjectRepository(db)
        rev = await revision_repo.get_by_git_sha(project_id, git_sha)
        if not rev:
            fallback = await revision_repo.get_by_source(project_id, "codegen")
            rev = await revision_repo.create(
                project_id=project_id,
                git_sha=git_sha,
                source="codegen",
                source_id=fallback.source_id if fallback else None,
                commit_message=_COMMIT_MSG,
            )
        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 project_repo.update(
            project_id,
            active_revision_id=rev.id,
            active_preview_id=pe.id,
        )
        await db.commit()
        preview_id, revision_id = pe.id, rev.id
    await enqueue_provision_preview_for_revision(str(preview_id), str(project_id), str(revision_id))


async def _head_sha(repo_path: str) -> str:
    proc = await asyncio.create_subprocess_exec(
        "git",
        "rev-parse",
        "HEAD",
        cwd=repo_path,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
    )
    stdout, _ = await proc.communicate()
    if proc.returncode != 0:
        return ""
    return (stdout or b"").decode().strip()


async def _commit_all(repo_path: str, message: str, *, git_name: str, git_email: str) -> bool:
    env = {
        **os.environ,
        "GIT_AUTHOR_NAME": git_name,
        "GIT_AUTHOR_EMAIL": git_email,
        "GIT_COMMITTER_NAME": git_name,
        "GIT_COMMITTER_EMAIL": git_email,
    }
    from src.workers.repo_ops import stage_publishable_and_assert

    try:
        staged = await stage_publishable_and_assert(repo_path)
    except Exception as exc:
        raise RuntimeError(f"git add failed: {exc}") from exc
    if not staged:
        return False
    commit = await asyncio.create_subprocess_exec(
        "git",
        "commit",
        "-m",
        message,
        cwd=repo_path,
        env=env,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
    )
    stdout, stderr = await commit.communicate()
    combined = (stdout or b"").decode() + (stderr or b"").decode()
    if commit.returncode != 0:
        if "nothing to commit" in combined.lower():
            return False
        raise RuntimeError(f"git commit failed: {combined}")
    return True
