"""Agent prompt construction, review-strategy selection, and memory index."""

import datetime as dt
import random as _random
import time
from pathlib import Path
from typing import Dict, List, Optional, Tuple

from common import REVIEW_STRATEGIES
from tasks import (
    extract_area,
    find_all_unplanned,
    find_planned_not_executed,
    group_all_tasks_by_area,
    load_tasks,
    next_task_number,
    summarize_all_executed_tasks,
    summarize_executed_tasks,
    topological_sort,
)
from status import load_journal


# ---------------------------------------------------------------------------
# Strategy selection — journal-informed weighted random
# ---------------------------------------------------------------------------

def pick_review_strategy(
    iteration: int,
    project_dir: Path = None,
    strategies: Optional[List[dict]] = None,
) -> dict:
    """Return the review strategy for this iteration.

    When *strategies* is provided it replaces the built-in pool.
    When *project_dir* is given and a journal exists, strategies that have
    historically been productive receive higher weight, and strategies that
    haven't been tried recently get a recency bonus.  Falls back to simple
    round-robin when no journal data is available.
    """
    pool = strategies if strategies else REVIEW_STRATEGIES
    if project_dir is None:
        idx = (iteration - 1) % len(pool)
        return pool[idx]

    entries = load_journal(project_dir, max_entries=80)
    review_entries = [e for e in entries if "review" in e.get("phases", [])]
    if len(review_entries) < 3:
        idx = (iteration - 1) % len(pool)
        return pool[idx]

    stats: Dict[str, Dict] = {}
    last_used: Dict[str, int] = {}
    for e in review_entries:
        name = e.get("strategy", "")
        if not name:
            continue
        stats.setdefault(name, {"productive": 0, "total": 0})
        stats[name]["total"] += 1
        if e.get("outcome") == "productive":
            stats[name]["productive"] += 1
        last_used[name] = e.get("iter", 0)

    weights: Dict[str, float] = {}
    for s in pool:
        name = s["name"]
        st = stats.get(name, {"productive": 0, "total": 0})
        rate = st["productive"] / st["total"] if st["total"] > 0 else 0.5
        rate = max(rate, 0.08)
        gap = iteration - last_used.get(name, 0)
        recency_bonus = min(gap / len(pool), 3.0)
        weights[name] = rate * (1.0 + recency_bonus)

    total_weight = sum(weights.values())
    r = _random.random() * total_weight
    cumulative = 0.0
    for s in pool:
        cumulative += weights[s["name"]]
        if r <= cumulative:
            return s
    return pool[-1]


# ---------------------------------------------------------------------------
# Compact review history — one prompt line summarizing recent outcomes
# ---------------------------------------------------------------------------

def format_review_history(project_dir: Path, max_show: int = 6) -> str:
    """Return a compact one-liner of recent review outcomes for the prompt.

    Example: ``surface-sweep→idle, deep-dive→+2, fresh-eyes→idle``
    """
    entries = load_journal(project_dir, max_entries=30)
    reviews = [e for e in entries if "review" in e.get("phases", [])]
    if not reviews:
        return ""
    shown = reviews[-max_show:]
    parts = []
    for e in shown:
        name = e.get("strategy", "?")
        outcome = e.get("outcome", "?")
        created = e.get("created", [])
        if outcome == "productive" and created:
            parts.append(f"{name}→+{len(created)}")
        elif outcome == "idle":
            parts.append(f"{name}→idle")
        else:
            parts.append(f"{name}→{outcome}")
    return "RECENT REVIEWS: " + ", ".join(parts)


# ---------------------------------------------------------------------------
# Memory index file — written before each iteration for agent context
# ---------------------------------------------------------------------------

def _format_relative_time(ts: int) -> str:
    """Human-friendly relative time from a unix timestamp."""
    delta = int(time.time()) - ts
    if delta < 60:
        return "just now"
    if delta < 3600:
        return f"{delta // 60}min ago"
    if delta < 86400:
        return f"{delta // 3600}h ago"
    return f"{delta // 86400}d ago"


def generate_memory_index(
    project_dir: Path,
    tasks_dir: Path,
    initiatives_base: Path,
    root_id: str = "",
) -> str:
    """Generate the ``.autobuilder/memory-index.md`` content.

    Contains strategy effectiveness, recent iteration history, and area
    coverage — everything the agent should know before starting a review.
    """
    now_str = dt.datetime.now().strftime("%Y-%m-%d %H:%M")
    entries = load_journal(project_dir, max_entries=80)
    review_entries = [e for e in entries if "review" in e.get("phases", [])]

    lines = [
        "# Iteration Memory",
        f"Updated: {now_str}",
        "",
    ]

    # -- Strategy effectiveness --
    if review_entries:
        stats: Dict[str, Dict] = {}
        for e in review_entries:
            name = e.get("strategy", "unknown")
            stats.setdefault(name, {"productive": 0, "total": 0, "last_ts": 0})
            stats[name]["total"] += 1
            if e.get("outcome") == "productive":
                stats[name]["productive"] += 1
            stats[name]["last_ts"] = max(stats[name]["last_ts"], e.get("ts", 0))

        lines.append("## Strategy Effectiveness")
        ranked = sorted(
            stats.items(),
            key=lambda x: (x[1]["productive"] / max(x[1]["total"], 1), x[1]["total"]),
            reverse=True,
        )
        for name, st in ranked:
            rate = int(100 * st["productive"] / st["total"]) if st["total"] else 0
            last = _format_relative_time(st["last_ts"]) if st["last_ts"] else "never"
            lines.append(
                f"- {name}: {st['productive']}/{st['total']} productive "
                f"({rate}%) — last productive: {last}"
            )
        lines.append("")

    # -- Recent iterations --
    recent = entries[-15:]
    if recent:
        lines.append("## Recent Iterations")
        for e in reversed(recent):
            it = e.get("iter", "?")
            strategy = e.get("strategy", "-")
            outcome = e.get("outcome", "?")
            created = e.get("created", [])
            executed = e.get("executed", [])
            ts = _format_relative_time(e.get("ts", 0))
            detail = ""
            if created:
                detail += f" created:{','.join(created)}"
            if executed:
                detail += f" executed:{','.join(executed)}"
            lines.append(f"- #{it} ({ts}) {strategy} → {outcome}{detail}")
        lines.append("")

    # -- Area coverage (across all initiatives) --
    area_groups = group_all_tasks_by_area(initiatives_base, root_id)
    if area_groups:
        lines.append("## Work Coverage")
        ranked_areas = sorted(area_groups.items(), key=lambda x: -len(x[1]))
        for area, titles in ranked_areas:
            latest = titles[-1] if titles else ""
            lines.append(f"- {area}: {len(titles)} tasks — latest: \"{latest}\"")
        lines.append("")

    # -- Pending work in active initiative --
    pending = find_planned_not_executed(tasks_dir)
    if pending:
        lines.append("## Pending Tasks (planned, not yet executed)")
        for iid, md in pending:
            lines.append(f"- {iid}: {md.stem}")
        lines.append("")

    return "\n".join(lines)


def _build_review_step(
    *,
    step: int,
    review_mode: str,
    review_strategy: dict,
    augment_review: str,
    next_num: int,
    tasks_rel: str,
    task_parent: str,
    screenshot_dir: Optional[str],
    screenshot_next_prefix: int,
    dev_server_url: Optional[str],
    test_credentials: Optional[tuple],
    max_tasks: Optional[int],
    browser_session: Optional[str],
    early_complete: bool = False,
) -> str:
    """Render the PRODUCT REVIEW step block."""
    strategy_name = review_strategy.get("name", "surface-sweep")
    strategy_focus = review_strategy.get("focus", "")

    parts: List[str] = []
    parts.append(
        f"STEP {step} — PRODUCT REVIEW\n"
        f"Read `.cursor/skills/autobuilder/product-review/SKILL.md` for "
        f"the full review methodology.\n"
        f"Read `.autobuilder/memory-index.md` (if present) for iteration "
        f"history and area coverage.\n"
    )

    if early_complete:
        parts.append(
            "\nEARLY-COMPLETE MODE — convergence-focused review.\n"
            "Only flag issues that fall into ONE of these buckets:\n"
            "  1. BROKEN FUNCTIONALITY — a documented feature does not work, "
            "errors out, returns wrong data, or crashes the app.\n"
            "  2. CRITICAL UX BLOCKERS — a primary user flow is unusable: "
            "blank pages, dead-end states, illegible text, controls that "
            "never respond, fatal navigation traps.\n"
            "  3. DATA / SECURITY HAZARDS — data loss, leaked secrets, "
            "privilege escalation, persistent corruption.\n"
            "Do NOT create tasks for: visual polish, spacing/typography "
            "tweaks, performance optimizations beyond hangs, new features, "
            "nice-to-haves, competitor parity beyond the three buckets "
            "above, or 'we could also add X'.\n"
            "If after a thorough review you find NO issues in those three "
            "buckets, write `.autobuilder/PRODUCTION_READY.md` with a brief "
            "summary of what you tested and why the app meets the "
            "early-complete bar. Creating zero tasks signals convergence.\n"
        )

    if dev_server_url and screenshot_dir:
        p = screenshot_next_prefix
        numbering_hint = (
            f"start at {p:02d} (previous screenshots use 01–{p - 1:02d})"
            if p > 1 else "start at 01"
        )
        sess = f" --session {browser_session}" if browser_session else ""
        ab = f"agent-browser{sess}"
        browser_block = (
            f"\nBROWSER & SCREENSHOTS:\n"
            f"  Dev server URL: {dev_server_url}\n"
            f"  Screenshot dir: {screenshot_dir}/\n"
            f"  Screenshot numbering: {numbering_hint}\n\n"
            f"  To take screenshots of the running app:\n\n"
            f"    {ab} open {dev_server_url} && "
            f"{ab} wait --load networkidle\n"
            f"    {ab} screenshot {screenshot_dir}/{p:02d}-landing.png\n"
        )
        if test_credentials:
            browser_block += (
                f"\n  Test account credentials:\n"
                f"    Username: {test_credentials[0]}\n"
                f"    Password: {test_credentials[1]}\n\n"
                f"  After login, navigate each main section and screenshot:\n"
                f"    {ab} screenshot {screenshot_dir}/{p + 1:02d}-<section>.png\n"
                f"  Continue incrementing the prefix for each screenshot.\n\n"
            )
        else:
            browser_block += "\n"
        browser_block += (
            f"  Use `{ab} snapshot -i` to inspect interactive elements.\n"
            f"  Use `{ab} screenshot --annotate` for labeled element screenshots.\n"
            f"  If any page looks visually broken, create a CRITICAL task for it.\n"
            f"\n  IMPORTANT: When you are done with the browser, always close it:\n"
            f"    {ab} close\n"
        )
        parts.append(browser_block)
    elif screenshot_dir:
        parts.append(
            f"\nSCREENSHOT DIR: {screenshot_dir}/\n"
            f"Save review screenshots to the directory above.\n"
        )

    max_tasks_line = ""
    if max_tasks is not None:
        max_tasks_line = f"  Maximum tasks for this review: {max_tasks}\n"
    parts.append(
        f"\nInputs for this step:\n"
        f"  Review mode: {review_mode}\n"
        f"  Review strategy: {strategy_name}\n"
        f"{max_tasks_line}"
        f"  Starting task number: {next_num:04d}\n"
        f"  Parent for new tasks: {task_parent}\n"
        f"  Tasks directory: {tasks_rel}\n"
        f"  Generate tasks ONLY within the initiative spec's scope "
        f"(unless an issue is CRITICAL — app crash, blank page, data loss).\n"
    )
    if strategy_focus:
        parts.append(
            f"\nREVIEW FOCUS FOR THIS ITERATION:\n"
            f"{strategy_focus}\n\n"
            f"Do NOT just do a generic surface sweep. The above focus is your "
            f"primary testing angle for this review. Spend most of your review "
            f"time on it. Only create tasks for issues you actually observe "
            f"from this angle.\n"
        )
    if augment_review:
        parts.append(
            f"\nADDITIONAL REVIEW INSTRUCTIONS (from project hooks):\n"
            f"{augment_review}\n"
        )
    return "\n".join(parts)


def _build_plan_step(
    *,
    step: int,
    augment_plan: str,
    unplanned: List,
    review_in_phases: bool,
    next_num: int,
    tasks_rel: str,
    task_subset: Optional[List] = None,
    task_progress: Optional[Tuple[int, int]] = None,
) -> str:
    """Render the PLAN TASKS step block.

    When *task_subset* is given, the prompt asks the agent to plan only
    those tasks (per-task fanout). Otherwise it falls back to the legacy
    "all unplanned tasks in this iteration" behaviour.
    """
    parts: List[str] = []
    progress_tag = ""
    if task_progress and task_progress[1] > 1:
        progress_tag = f" — call {task_progress[0]} of {task_progress[1]}"

    parts.append(
        f"STEP {step} — PLAN TASKS{progress_tag}\n"
        f"Use the `plan-task` skill.\n\n"
        f"Read `.cursor/skills/autobuilder/plan-task/SKILL.md` and follow its instructions.\n\n"
    )

    if task_subset is not None:
        # Per-task fanout: address only the listed task(s).
        n = len(task_subset)
        if n == 1:
            tid, md = task_subset[0]
            parts.append(
                f"Plan EXACTLY this one task — do NOT touch any other "
                f"task file in this call:\n"
                f"  - {tasks_rel}{md.name} (id: {tid})\n\n"
                f"Run the full planning workflow on it:\n"
                f"research → architecture diagram → one-week decision → "
                f"split if needed → set planned: true.\n"
            )
        else:
            parts.append(
                f"Plan EXACTLY these {n} task(s) — do NOT touch any other "
                f"task file in this call:\n"
            )
            for tid, md in task_subset:
                parts.append(f"  - {tasks_rel}{md.name} (id: {tid})")
            parts.append(
                "\nFor each one, run the full planning workflow:\n"
                "research → architecture diagram → one-week decision → "
                "split if needed → set planned: true.\n"
            )
        parts.append(
            f"\nNext available number for splits: {next_num:04d}\n"
            f"If you split a task, create child files with planned: false. "
            f"The build loop will plan those children on the next call.\n"
        )
    else:
        # Legacy: address all unplanned tasks in one prompt.
        unplanned_list = unplanned if not review_in_phases else []
        parts.append(
            "For EVERY task with `planned: false`, run the full planning "
            "workflow:\nresearch → architecture diagram → one-week decision "
            "→ split if needed → set planned: true.\n\n"
        )
        if unplanned_list:
            parts.append("Tasks to plan:")
            for tid, md in unplanned_list:
                parts.append(f"  - {tasks_rel}{md.name} (id: {tid})")
            parts.append(f"\nNext available number for splits: {next_num:04d}\n")
        else:
            parts.append(
                "The tasks you created in the previous step have planned: false.\n"
                "Each one MUST go through planning before execution.\n"
            )
        parts.append(
            "If splitting, create child files with planned: false, then plan each child.\n"
            "Continue until ALL tasks have planned: true.\n"
        )

    if augment_plan:
        parts.append(
            f"ADDITIONAL PLANNING INSTRUCTIONS (from project hooks):\n"
            f"{augment_plan}\n\n"
        )
    return "\n".join(parts)


def _build_execute_step(
    *,
    step: int,
    augment_execute: str,
    planned_pending: List,
    all_tasks: dict,
    already_executed: set,
    review_in_phases: bool,
    plan_in_phases: bool,
    tasks_rel: str,
    task_subset: Optional[List] = None,
    task_progress: Optional[Tuple[int, int]] = None,
) -> str:
    """Render the EXECUTE TASKS step block.

    When *task_subset* is given, the prompt asks the agent to execute
    only those tasks (per-task fanout). Otherwise it falls back to the
    legacy "all planned-but-unexecuted tasks in this iteration" path.
    """
    parts: List[str] = []
    progress_tag = ""
    if task_progress and task_progress[1] > 1:
        progress_tag = f" — call {task_progress[0]} of {task_progress[1]}"

    parts.append(
        f"STEP {step} — EXECUTE TASKS{progress_tag}\n"
        f"Use the `execute-task` skill.\n\n"
        f"Read `.cursor/skills/autobuilder/execute-task/SKILL.md` and follow its instructions.\n\n"
    )

    if task_subset is not None:
        n = len(task_subset)
        if n == 1:
            tid, md_path = task_subset[0]
            task = all_tasks.get(tid)
            title = task["title"] if task else md_path.stem
            deps = task["deps"] if task else []
            parts.append(
                "Execute EXACTLY this one task — do NOT touch any other "
                "task file in this call:\n"
                f"  - {tasks_rel}{md_path.name} — {title}\n"
            )
            if deps:
                parts.append(f"    deps: {deps}")
            parts.append(
                "\nMake ONE commit when done (git add -A && git commit). "
                "Then stop — do not start the next task; the build loop "
                "will dispatch it as a separate call.\n"
            )
        else:
            parts.append(
                f"Execute EXACTLY these {n} task(s) in dependency order — "
                "do NOT touch any other task file in this call:\n"
            )
            for tid, md_path in task_subset:
                task = all_tasks.get(tid)
                title = task["title"] if task else md_path.stem
                deps = task["deps"] if task else []
                parts.append(f"  - {tasks_rel}{md_path.name} — {title}")
                if deps:
                    parts.append(f"    deps: {deps}")
            parts.append(
                "\nMake ONE commit per task (git add -A && git commit after each).\n"
            )
    else:
        # Legacy: execute everything in one call.
        if planned_pending and not review_in_phases and not plan_in_phases:
            exec_order = topological_sort(all_tasks, already_executed)
            parts.append("Execute in this order:")
            for tid in exec_order:
                task = all_tasks[tid]
                parts.append(f"  - {tasks_rel}{task['path'].name} — {task['title']}")
                if task["deps"]:
                    parts.append(f"    deps: {task['deps']}")
            parts.append("")
        else:
            parts.append(
                "Execute all tasks that have planned: true and executed: false.\n"
                "Respect dependency order.\n"
                "Make ONE commit per task (git add -A && git commit after each).\n"
            )

    parts.append(
        "Do NOT run git push — the build loop handles pushing.\n\n"
        "IMPORTANT: After implementing each task, run react-doctor before committing:\n"
        "  npx -y react-doctor@latest . --verbose --diff\n"
        "Fix all errors. Target score 75+. Do NOT commit if score is below 50.\n"
    )
    if augment_execute:
        parts.append(
            f"ADDITIONAL EXECUTION INSTRUCTIONS (from project hooks):\n"
            f"{augment_execute}\n\n"
        )
    return "\n".join(parts)


_CRITICAL_RULES = (
    "\n" + "=" * 60 + "\n"
    "CRITICAL RULES:\n"
    "- NEVER modify files with executed: true — they are LOCKED.\n"
    "- NEVER run git push — the build loop handles pushing.\n"
    "- For UI changes, provide a polished, modern look and feel.\n"
    "- Follow the task plan precisely.\n"
)


def _build_reference_material(
    *,
    project_dir: Path,
    tasks_dir: Path,
    root_initiative: str,
    initiative_spec: str,
    initiative_constraints: str,
    project_context: str,
    global_executed_summary: str,
    env_vars: List[str],
) -> str:
    """Render the REFERENCE MATERIAL block.

    To minimise per-token cost (the iteration header is replayed via
    chat-history on every chained agent call), spec / constraints /
    PRODUCT.md are referenced by **file path** rather than inlined when
    the underlying file actually exists. The agent reads them on demand
    via its `Read` tool. Inline fallback only kicks in for legacy
    projects where the synthesised content has no on-disk file.
    """
    review_history = format_review_history(project_dir)
    local_executed = summarize_executed_tasks(
        tasks_dir, max_recent=10, root_id=root_initiative,
    )

    parts: List[str] = []
    parts.append("\n" + "=" * 60)
    parts.append("REFERENCE MATERIAL")
    parts.append("=" * 60 + "\n")

    # ── REQUIRED READING — file paths the agent must read each phase ──
    # We list a path only when the file is non-empty on disk; that way
    # the agent never gets a dangling reference. Each path is shown
    # relative to project_dir for portability.
    init_dir = tasks_dir.parent  # .autobuilder/initiatives/<id>
    spec_path = init_dir / "spec.md"
    constraints_path = init_dir / "constraints.md"
    product_md = project_dir / ".autobuilder" / "PRODUCT.md"
    memory_index = project_dir / ".autobuilder" / "memory-index.md"

    def _rel(p: Path) -> str:
        try:
            return p.resolve().relative_to(project_dir.resolve()).as_posix()
        except ValueError:
            return p.as_posix()

    def _has_content(p: Path) -> bool:
        if not p.exists():
            return False
        try:
            return bool(p.read_text(encoding="utf-8").strip())
        except Exception:
            return False

    reading: List[str] = []
    if _has_content(spec_path):
        reading.append(
            f"  - {_rel(spec_path)}  — active-initiative scope (PRIMARY focus for this iteration)"
        )
    if _has_content(constraints_path):
        reading.append(
            f"  - {_rel(constraints_path)}  — initiative-specific constraints"
        )
    if _has_content(product_md):
        reading.append(
            f"  - {_rel(product_md)}  — product-level background, vision, principles"
        )
    # memory-index is regenerated each iteration (review phase always
    # writes one). It carries strategy effectiveness, recent iteration
    # outcomes, area coverage, and the pending-task list — so we always
    # reference it, but only when it actually exists on disk.
    if _has_content(memory_index):
        reading.append(
            f"  - {_rel(memory_index)}  — recent strategies, area coverage, pending tasks"
        )

    if reading:
        parts.append(
            "REQUIRED READING — read these files at the start of each phase "
            "(via your Read tool); do not assume their content from this prompt:"
        )
        parts.extend(reading)
        parts.append("")

    # Compact summaries (kept inline because they're computed, not stored).
    if review_history:
        parts.append(review_history + "\n")

    if global_executed_summary:
        parts.append(
            f"ALREADY BUILT (all initiatives — do not redo or duplicate):\n"
            f"{global_executed_summary}\n"
        )
    elif local_executed:
        parts.append(f"ALREADY BUILT (do not redo or duplicate):\n{local_executed}\n")

    # Legacy fallback: inline only when no on-disk file backs the content.
    # ``initiative_spec`` / ``initiative_constraints`` are read from
    # ``spec.md`` / ``constraints.md`` upstream, so if those files don't
    # exist we have no path to reference and fall back to inlining.
    if initiative_spec and not _has_content(spec_path):
        parts.append(
            f"INITIATIVE SPEC (your scope for this iteration):\n"
            f"---\n{initiative_spec}\n---\n"
        )
    if initiative_constraints and not _has_content(constraints_path):
        parts.append(
            f"INITIATIVE CONSTRAINTS:\n"
            f"---\n{initiative_constraints}\n---\n"
        )
    # ``project_context`` may be PRODUCT.md OR a synthesised legacy
    # blob (scope.md + constraints.md merged). Reference PRODUCT.md when
    # it exists; otherwise inline (legacy projects).
    if project_context and not _has_content(product_md):
        parts.append(
            f"PROJECT CONTEXT (legacy — no PRODUCT.md found):\n"
            f"---\n{project_context}\n---\n"
        )
    if env_vars:
        parts.append(
            f"CREDENTIALS (.env at project root):\n"
            + "\n".join(f"  {v}" for v in env_vars) + "\n"
            "Load these with dotenv. Use them to verify against the live API.\n"
        )

    return "\n".join(parts)


def build_iteration_header(
    *,
    project_dir: Path,
    project_id: str,
    root_initiative: str,
    tasks_dir: Path,
    tasks_rel: str,
    active_initiative: str,
    initiative_spec: str,
    initiative_constraints: str,
    iteration: int,
    backlog: str,
    env_vars: List[str],
    port: Optional[int],
    project_context: str,
    initiative_map: str,
    global_executed_summary: str,
) -> str:
    """Build the shared iteration header sent only with the first phase.

    Includes the build-loop banner, project metadata, initiative map,
    backlog, and the full reference-material block. Subsequent phases
    re-use the chat history rather than re-sending this content.
    """
    next_num = next_task_number(tasks_dir)

    parts: List[str] = []
    parts.append(
        f"THIS IS AN AUTOMATED BUILD LOOP — iteration #{iteration}.\n"
        f"Do NOT ask questions or wait for input. I will send each phase "
        f"as a separate request; the chat history carries between calls.\n"
    )

    parts.append(f"PROJECT: {project_id}")
    parts.append(f"PROJECT DIR: {project_dir}")
    parts.append(f"ACTIVE INITIATIVE: {active_initiative}")
    parts.append(f"TASKS DIR: {tasks_rel}")
    parts.append(f"NEXT TASK NUMBER: {next_num:04d}")
    parts.append(f"ROOT ID: {root_initiative}")
    if port:
        parts.append(f"DEV SERVER PORT: {port}")
    parts.append("")

    if initiative_map:
        parts.append(f"INITIATIVES:\n{initiative_map}\n")

    if backlog:
        parts.append(
            f"USER FEEDBACK (highest priority — address before your own review):\n"
            f"---\n{backlog}\n---\n"
            f"The feedback above comes directly from the product owner. "
            f"Address it before picking your own changes.\n"
        )

    parts.append(_build_reference_material(
        project_dir=project_dir,
        tasks_dir=tasks_dir,
        root_initiative=root_initiative,
        initiative_spec=initiative_spec,
        initiative_constraints=initiative_constraints,
        project_context=project_context,
        global_executed_summary=global_executed_summary,
        env_vars=env_vars,
    ))
    return "\n".join(parts)


def _build_phase_reminder(
    *,
    project_id: str,
    active_initiative: str,
    tasks_rel: str,
    next_num: int,
    iteration: int,
    phase: str,
) -> str:
    """Render a small reminder header for non-first phase prompts.

    The agent already has the iteration header in chat history; this just
    re-anchors the metadata so cross-phase context isn't lost in long runs.
    """
    return (
        f"AUTOMATED BUILD LOOP — iteration #{iteration}, phase: {phase.upper()}.\n"
        f"PROJECT: {project_id} | INITIATIVE: {active_initiative}\n"
        f"TASKS DIR: {tasks_rel} | NEXT TASK NUMBER: {next_num:04d}\n"
        f"Do NOT ask questions or wait for input. Continue from where you "
        f"left off in this chat.\n"
    )


def build_phase_prompt(
    *,
    phase: str,
    include_header: bool,
    project_dir: Path,
    project_id: str,
    root_initiative: str,
    tasks_dir: Path,
    tasks_rel: str,
    active_initiative: str,
    initiative_spec: str,
    initiative_constraints: str,
    iteration: int,
    backlog: str,
    phases: List[str],
    review_mode: str,
    review_strategy: dict,
    env_vars: List[str],
    port: Optional[int] = None,
    project_context: str = "",
    initiative_map: str = "",
    global_executed_summary: str = "",
    augment_review: str = "",
    augment_plan: str = "",
    augment_execute: str = "",
    screenshot_dir: Optional[str] = None,
    screenshot_next_prefix: int = 1,
    dev_server_url: Optional[str] = None,
    test_credentials: Optional[tuple] = None,
    max_tasks: Optional[int] = None,
    browser_session: Optional[str] = None,
    early_complete: bool = False,
    task_subset: Optional[List] = None,
    task_progress: Optional[Tuple[int, int]] = None,
) -> str:
    """Build a focused single-phase prompt.

    When *include_header* is True, the full iteration header (project info,
    initiative map, backlog, reference material) is prepended — use this
    for the FIRST phase of an iteration. Otherwise a compact one-line
    reminder is used; the chat history carries the rest.

    *task_subset* (with optional *task_progress* = ``(idx, total)``) lets
    the caller fan a phase out into multiple per-task agent calls. It
    only applies to the ``plan`` and ``execute`` phases; ``review`` is
    one call regardless.
    """
    next_num = next_task_number(tasks_dir)
    all_tasks = load_tasks(tasks_dir)
    already_executed = {tid for tid, t in all_tasks.items() if t["executed"]}
    unplanned = find_all_unplanned(tasks_dir)
    planned_pending = find_planned_not_executed(tasks_dir)
    task_parent = root_initiative

    parts: List[str] = []

    if include_header:
        parts.append(build_iteration_header(
            project_dir=project_dir,
            project_id=project_id,
            root_initiative=root_initiative,
            tasks_dir=tasks_dir,
            tasks_rel=tasks_rel,
            active_initiative=active_initiative,
            initiative_spec=initiative_spec,
            initiative_constraints=initiative_constraints,
            iteration=iteration,
            backlog=backlog,
            env_vars=env_vars,
            port=port,
            project_context=project_context,
            initiative_map=initiative_map,
            global_executed_summary=global_executed_summary,
        ))
    else:
        parts.append(_build_phase_reminder(
            project_id=project_id,
            active_initiative=active_initiative,
            tasks_rel=tasks_rel,
            next_num=next_num,
            iteration=iteration,
            phase=phase,
        ))

    parts.append("=" * 60)
    parts.append(f"EXECUTION — Run STEP 1 below now.")
    parts.append("=" * 60 + "\n")

    if phase == "review":
        parts.append(_build_review_step(
            step=1,
            review_mode=review_mode,
            review_strategy=review_strategy,
            augment_review=augment_review,
            next_num=next_num,
            tasks_rel=tasks_rel,
            task_parent=task_parent,
            screenshot_dir=screenshot_dir,
            screenshot_next_prefix=screenshot_next_prefix,
            dev_server_url=dev_server_url,
            test_credentials=test_credentials,
            max_tasks=max_tasks,
            browser_session=browser_session,
            early_complete=early_complete,
        ))
    elif phase == "plan":
        parts.append(_build_plan_step(
            step=1,
            augment_plan=augment_plan,
            unplanned=unplanned,
            review_in_phases="review" in phases,
            next_num=next_num,
            tasks_rel=tasks_rel,
            task_subset=task_subset,
            task_progress=task_progress,
        ))
    elif phase == "execute":
        parts.append(_build_execute_step(
            step=1,
            augment_execute=augment_execute,
            planned_pending=planned_pending,
            all_tasks=all_tasks,
            already_executed=already_executed,
            review_in_phases="review" in phases,
            plan_in_phases="plan" in phases,
            tasks_rel=tasks_rel,
            task_subset=task_subset,
            task_progress=task_progress,
        ))
    else:
        raise ValueError(f"Unknown phase: {phase!r}")

    parts.append(_CRITICAL_RULES)
    return "\n".join(parts)


