"""Task: Legacy / edge-case tree application (mostly superseded by single-phase setup).

New generated Pages projects assemble in ``run_project_setup`` (runtime substrate
+ starter_assembly — no worker build). This task remains for:
  - Rows stuck at ``setup_stage=skeleton_pushed`` from older two-phase deployments
  - Cloning + applying when the local workspace is missing on a different worker host
"""
from __future__ import annotations

import json
import os

import structlog

logger = structlog.get_logger()


def _boilerplate_tree_present(repo_path: str, project_type: str) -> bool:
    """Heuristic that boilerplate content is already on disk (e.g. arq retry after commit, before DB)."""
    try:
        if project_type == "nuxt_app":
            return os.path.isfile(os.path.join(repo_path, "nuxt.config.ts")) or os.path.isfile(
                os.path.join(repo_path, "nuxt.config.mjs"),
            )
        if project_type == "react_app":
            return os.path.isfile(os.path.join(repo_path, "vite.config.ts")) or os.path.isfile(
                os.path.join(repo_path, "vite.config.js"),
            )
        if project_type == "html":
            p = os.path.join(repo_path, ".p2hs.json")
            if os.path.isfile(p):
                with open(p, encoding="utf-8") as fh:
                    data = json.load(fh)
                return data.get("project_type") == "html" or data.get("type") == "html"
        return False
    except Exception:
        return False


async def apply_boilerplate_snapshot(ctx: dict, project_id: str) -> dict:
    from uuid import UUID

    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.projects.repository import ProjectRepository, ProjectRepoRepository
    from src.integrations.github_client import GitHubAppClient
    from src.integrations.github_installation import resolve_installation_id_for_workspace
    from src.workers.boilerplate_store import (
        apply_live_boilerplate,
        apply_snapshot,
        find_latest_version,
        get_snapshot_dir,
    )
    from src.workers.repo_ops import (
        create_or_reset_preview_branch,
        ensure_git_remote_push,
        get_project_repo_path,
        git_rev_parse_head,
        prepare_working_tree,
        push_branch,
    )

    db_factory = ctx.get("db_factory", async_session_factory)
    settings = get_settings()
    pid = UUID(project_id)

    # --- Load project state ---------------------------------------------------
    async with db_factory() as db:
        project = await ProjectRepository(db).get_by_id(pid)
        if not project:
            raise RuntimeError(f"Project {project_id} not found")
        pr_row = await ProjectRepoRepository(db).get_by_project_id(pid)
        if not pr_row:
            raise RuntimeError(f"ProjectRepo not found for {project_id}")
        if pr_row.setup_stage == "completed":
            logger.info("apply_boilerplate_already_done", project_id=project_id)
            return {"status": "ok", "skipped": True, "reason": "already_completed"}

        project_type = project.type
        project_ref = (project.project_ref or "").strip()
        project_created_at = project.created_at
        if not project_ref:
            raise RuntimeError(f"Project {project_id} has no project_ref; cannot finalize deploy workflow")
        workspace_id = project.workspace_id
        repo_full_name = pr_row.repo_full_name
        default_branch = pr_row.default_branch or "main"
        preview_branch = pr_row.preview_branch or settings.hosting_preview_branch or "preview"

    repo_path = get_project_repo_path(settings.repo_workspace, workspace_id, pid)

    # --- Resolve authenticated push URL (needed for clone heal + push) ---------
    installation_id = await resolve_installation_id_for_workspace(db_factory, workspace_id)
    github = GitHubAppClient(installation_id=installation_id)
    auth_url = await github.get_authenticated_clone_url(repo_full_name)

    # Setup usually leaves a local repo on the worker that ran it. If this task
    # runs on another host (horizontal workers, ephemeral disk), clone from GitHub.
    local_head = await git_rev_parse_head(repo_path)
    if not local_head:
        logger.warning(
            "apply_boilerplate_local_missing_cloning",
            project_id=project_id,
            repo_path=repo_path,
            repo_full_name=repo_full_name,
            default_branch=default_branch,
            hint="Setup ran elsewhere or workspace was cleared; cloning default branch from GitHub.",
        )
        await prepare_working_tree(repo_path, auth_url, branch=default_branch)
        local_head = await git_rev_parse_head(repo_path)
        if not local_head:
            raise RuntimeError(
                f"Could not clone or open repo at {repo_path} from {repo_full_name} "
                f"(branch {default_branch}). Default branch may not exist on the remote yet."
            )

    # --- Apply generated substrate or legacy snapshot -------------------------
    from src.domain.starter_library.runtime_substrate import uses_starter_assembly

    if uses_starter_assembly(project_type):
        from src.domain.starter_library.runtime_substrate import write_generated_project_tree

        version = "starter_assembly"
        if not _boilerplate_tree_present(repo_path, project_type):
            write_generated_project_tree(repo_path, project_type)
        else:
            logger.info(
                "apply_boilerplate_skip_existing_pages_tree",
                project_id=project_id,
                project_type=project_type,
                mode="starter_assembly",
            )
    else:
        snapshot_dir = get_snapshot_dir(settings)
        version = find_latest_version(project_type, snapshot_dir)

        if version:
            used_snapshot = apply_snapshot(repo_path, project_type, version, snapshot_dir)
        else:
            used_snapshot = False

        if not used_snapshot:
            apply_live_boilerplate(repo_path, project_type)
            version = "live"
            logger.info(
                "apply_boilerplate_using_live_fallback",
                project_id=project_id,
                project_type=project_type,
            )

    # --- Write / refresh deploy workflow --------------------------------------
    workflow_dir = os.path.join(repo_path, ".github", "workflows")
    os.makedirs(workflow_dir, exist_ok=True)
    from src.domain.deployments.repository import DeployRepository

    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
    )
    with open(os.path.join(workflow_dir, "deploy.yml"), "w", encoding="utf-8") as fh:
        fh.write(deploy_workflow_for_project(
            project_type,
            project_ref,
            project_created_at,
            settings,
            primary_hosting_provider=getattr(project, "primary_hosting_provider", None),
            assume_unassigned_w4p=not has_successful_pages_deployment,
            has_successful_pages_deployment=has_successful_pages_deployment,
        ))

    # --- Single commit: boilerplate files + upgraded deploy workflow ----------
    env = {
        **os.environ,
        "GIT_AUTHOR_NAME": settings.git_committer_name,
        "GIT_AUTHOR_EMAIL": settings.git_committer_email,
        "GIT_COMMITTER_NAME": settings.git_committer_name,
        "GIT_COMMITTER_EMAIL": settings.git_committer_email,
    }
    import asyncio

    rc, _, err = await _git(repo_path, "checkout", default_branch)
    if rc != 0:
        raise RuntimeError(f"git checkout {default_branch} failed: {err}")

    from src.workers.repo_ops import stage_publishable_and_assert

    await stage_publishable_and_assert(repo_path)

    proc = await asyncio.create_subprocess_exec(
        "git", "commit", "-m", f"feat: apply {project_type} tree v{version}",
        cwd=repo_path, env=env,
        stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
    )
    stdout, stderr = await proc.communicate()
    out_text = (stdout or b"").decode() + (stderr or b"").decode()
    if proc.returncode != 0:
        if (
            "nothing to commit" in out_text.lower()
            and _boilerplate_tree_present(repo_path, project_type)
        ):
            logger.info(
                "apply_boilerplate_idempotent_skip_commit",
                project_id=project_id,
                project_type=project_type,
                reason="working_tree_already_matches_boilerplate",
            )
        elif "nothing to commit" in out_text.lower():
            raise RuntimeError(
                "Boilerplate apply produced no git changes (nothing to commit). "
                "If using snapshots, ensure nuxt_app@*.tar.gz (or live boilerplates/nuxt_app) exists on "
                "the worker; otherwise the repo may still show pre-boilerplate content."
            )
        else:
            raise RuntimeError(f"git commit (boilerplate) failed: {out_text}")

    # --- Push main + preview --------------------------------------------------
    await ensure_git_remote_push(repo_path, auth_url, default_branch)
    await create_or_reset_preview_branch(repo_path, preview_branch, from_branch=default_branch)
    await push_branch(repo_path, preview_branch, force=True)

    sha = await git_rev_parse_head(repo_path) or ""

    # --- Persist -----------------------------------------------------------
    async with db_factory() as db:
        await ProjectRepository(db).update(pid, boilerplate_version=version)
        await ProjectRepoRepository(db).update_setup_state(
            pid,
            setup_stage="completed",
            initial_commit_sha=sha or None,
        )
        await db.commit()

    logger.info(
        "apply_boilerplate_complete",
        project_id=project_id,
        project_type=project_type,
        version=version,
        sha=sha[:12] if sha else "",
    )
    return {"status": "ok", "version": version, "sha": sha}


async def _git(cwd: str, *args: str):
    import asyncio
    proc = await asyncio.create_subprocess_exec(
        "git", *args,
        cwd=cwd,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
    )
    stdout, stderr = await proc.communicate()
    return proc.returncode, (stdout or b"").decode().strip(), (stderr or b"").decode().strip()
