#!/usr/bin/env python3
"""Boris - Autonomous Project Orchestrator. Breaks tasks into milestones, executes via Claude CLI."""
import argparse
import logging
import os
import sys
from datetime import datetime

# Force unbuffered stdout so all output shows immediately on Windows
if hasattr(sys.stdout, "reconfigure"):
    try:
        sys.stdout.reconfigure(line_buffering=True)
    except Exception:
        pass
os.environ["PYTHONUNBUFFERED"] = "1"

import engine
import git_manager
import prompts
import state as state_module
from engine import Verdict

# Config constants (inlined from config.py)
BORIS_DIR = os.path.dirname(os.path.abspath(__file__))
PLANS_DIR = os.path.join(BORIS_DIR, "plans")
LOGS_DIR = os.path.join(BORIS_DIR, "logs")
DEFAULT_MAX_ITERATIONS = 15
MAX_CORRECTIONS = 2
MAX_RETRIES = 1


def setup_logging() -> logging.Logger:
    """Set up logging to both console and file."""
    os.makedirs(LOGS_DIR, exist_ok=True)
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    log_file = os.path.join(LOGS_DIR, f"boris_{timestamp}.log")

    logger = logging.getLogger("boris")
    logger.setLevel(logging.DEBUG)

    # File handler - detailed
    fh = logging.FileHandler(log_file, encoding="utf-8")
    fh.setLevel(logging.DEBUG)
    fh.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s"))

    # Console handler - info level
    # Use sys.stdout but wrap with utf-8 on Windows to handle emoji/unicode in log messages
    console_stream = sys.stdout
    if hasattr(sys.stdout, "reconfigure"):
        try:
            sys.stdout.reconfigure(encoding="utf-8", errors="replace")
        except Exception:
            pass
    ch = logging.StreamHandler(console_stream)
    ch.setLevel(logging.INFO)
    ch.setFormatter(logging.Formatter("[Boris] %(message)s"))

    logger.addHandler(fh)
    logger.addHandler(ch)

    return logger


def print_banner():
    """Print Boris startup banner."""
    print("=" * 60, flush=True)
    print("  BORIS - DaveLoop Orchestrator", flush=True)
    print("  Breaking tasks into milestones, delegating to DaveLoop", flush=True)
    print("=" * 60, flush=True)
    print(flush=True)


def print_plan_summary(plan: state_module.Plan):
    """Print a summary of the plan."""
    print(f"\nPlan: {plan.task}", flush=True)
    print(f"Milestones: {len(plan.milestones)}", flush=True)
    print("-" * 40, flush=True)
    for m in plan.milestones:
        deps = f" (depends on: {', '.join(m.depends_on)})" if m.depends_on else ""
        print(f"  {m.id}: {m.title}{deps}", flush=True)
    print("-" * 40, flush=True)
    print(flush=True)


def generate_summary(plan: state_module.Plan, project_dir: str, start_time: datetime) -> str:
    """Generate a summary markdown file and return its path."""
    os.makedirs(PLANS_DIR, exist_ok=True)
    end_time = datetime.now()
    duration = end_time - start_time

    completed = [m for m in plan.milestones if m.status == "completed"]
    skipped = [m for m in plan.milestones if m.status == "skipped"]
    failed = [m for m in plan.milestones if m.status not in ("completed", "skipped")]
    total = len(plan.milestones)

    hours, remainder = divmod(int(duration.total_seconds()), 3600)
    minutes, seconds = divmod(remainder, 60)
    duration_str = f"{hours}h {minutes}m {seconds}s" if hours else f"{minutes}m {seconds}s"

    lines = [
        "# Boris Orchestration Summary",
        "",
        f"**Task:** {plan.task}",
        f"**Project:** {project_dir}",
        f"**Started:** {start_time.strftime('%Y-%m-%d %H:%M:%S')}",
        f"**Finished:** {end_time.strftime('%Y-%m-%d %H:%M:%S')}",
        f"**Duration:** {duration_str}",
        "",
        "---",
        "",
        "## Results",
        "",
        f"| Metric | Count |",
        f"|--------|-------|",
        f"| Total milestones | {total} |",
        f"| Completed | {len(completed)} |",
        f"| Skipped | {len(skipped)} |",
        f"| Remaining | {len(failed)} |",
        "",
        f"**Overall: {'ALL MILESTONES COMPLETED' if len(completed) == total else 'PARTIAL COMPLETION'}**",
        "",
        "---",
        "",
        "## Milestone Breakdown",
        "",
    ]

    status_icons = {"completed": "+", "skipped": "!", "pending": " ", "in_progress": ">"}

    for m in plan.milestones:
        icon = status_icons.get(m.status, "?")
        lines.append(f"### [{icon}] {m.id}: {m.title}")
        lines.append(f"**Status:** {m.status.upper()}")
        if m.completed_at:
            lines.append(f"**Completed at:** {m.completed_at}")
        if m.files_to_create:
            lines.append(f"**Files created:** {', '.join(m.files_to_create)}")
        if m.files_to_modify:
            lines.append(f"**Files modified:** {', '.join(m.files_to_modify)}")
        if m.status == "skipped":
            lines.append(f"**Reason:** Skipped after exhausting retries/corrections")
        lines.append("")

    if skipped:
        lines.append("---")
        lines.append("")
        lines.append("## Skipped Milestones")
        lines.append("")
        for m in skipped:
            deps = f" (depends on: {', '.join(m.depends_on)})" if m.depends_on else ""
            lines.append(f"- **{m.id}: {m.title}**{deps} - skipped after failed retries/corrections")
        lines.append("")

    lines.append("---")
    lines.append("")
    lines.append("*Generated by Boris - Autonomous Project Orchestrator*")

    timestamp = end_time.strftime("%Y%m%d_%H%M%S")
    filepath = os.path.join(PLANS_DIR, f"summary_{timestamp}.md")
    with open(filepath, "w", encoding="utf-8") as f:
        f.write("\n".join(lines))

    return filepath


def print_final_summary(plan: state_module.Plan, project_dir: str, start_time: datetime):
    """Print final summary to console and save summary markdown."""
    completed = sum(1 for m in plan.milestones if m.status == "completed")
    skipped = sum(1 for m in plan.milestones if m.status == "skipped")
    total = len(plan.milestones)

    # Generate summary file
    summary_path = generate_summary(plan, project_dir, start_time)

    print(flush=True)
    print("=" * 60, flush=True)
    print("  BORIS - Orchestration Summary", flush=True)
    print("=" * 60, flush=True)
    print(f"  Completed: {completed}/{total}", flush=True)
    if skipped:
        print(f"  Skipped:   {skipped}/{total}", flush=True)
    print(flush=True)
    for m in plan.milestones:
        status_icon = {"completed": "+", "skipped": "-", "pending": " ", "in_progress": ">"}.get(
            m.status, "?"
        )
        print(f"  [{status_icon}] {m.id}: {m.title} ({m.status})", flush=True)
    print(flush=True)
    print(f"  Summary saved to: {summary_path}", flush=True)
    print(flush=True)
    if completed == total:
        print("Boris orchestration complete! All milestones delivered.", flush=True)
    else:
        print(f"Boris orchestration finished. {skipped} milestone(s) skipped.", flush=True)
    print("=" * 60, flush=True)


def parse_args() -> argparse.Namespace:
    """Parse CLI arguments."""
    parser = argparse.ArgumentParser(
        prog="boris",
        description="Boris - Autonomous Project Orchestrator. Breaks tasks into milestones and executes via Claude CLI.",
    )
    parser.add_argument("task", nargs="?", help="The task description string")
    parser.add_argument(
        "-d", "--dir", default=os.getcwd(), help="Working directory for the project (default: current dir)"
    )
    parser.add_argument(
        "-r", "--resume", action="store_true", help="Resume from last saved state"
    )
    parser.add_argument(
        "--plan-only", action="store_true", help="Generate plan but don't execute"
    )
    parser.add_argument("--remote", help="Git remote URL to set up")
    parser.add_argument(
        "--no-git", action="store_true", help="Disable git management"
    )
    parser.add_argument(
        "--max-iter",
        type=int,
        default=DEFAULT_MAX_ITERATIONS,
        help=f"Max DaveLoop iterations per milestone (default: {DEFAULT_MAX_ITERATIONS})",
    )
    parser.add_argument(
        "--skip-ui", action="store_true", help="Skip UI testing phase after structural milestones"
    )

    return parser.parse_args()


def _run_ui_phase(st, plan, project_dir, args, logger):
    """Phase 2: UI Testing & Polish. Boris ships DaveLoop into UI tester mode."""
    skip_ui = getattr(args, "skip_ui", False)
    if skip_ui:
        print("[Boris] --skip-ui flag set. Skipping UI testing phase.", flush=True)
        logger.info("UI testing phase skipped by --skip-ui flag")
        return

    structural_completed = sum(1 for m in plan.milestones if m.status == "completed")
    structural_total = len(plan.milestones)

    if structural_completed < structural_total:
        logger.info("Not all structural milestones complete (%d/%d), skipping UI phase",
                     structural_completed, structural_total)
        return

    # Transition to UI phase
    st.phase = "ui_testing"
    state_module.save(st)

    print(flush=True)
    print("=" * 60, flush=True)
    print("  BORIS - Phase 2: UI Testing & Polish", flush=True)
    print("=" * 60, flush=True)
    print(flush=True)
    print("[Boris] All structural milestones complete. Entering UI Testing & Polish phase...", flush=True)
    logger.info("Entering UI Testing & Polish phase")

    # Create UI plan if we don't have one yet.
    # Claude figures out the project type and test tool from the task + file listing.
    if st.ui_plan is None:
        print("[Boris] Creating UI testing plan...", flush=True)
        ui_plan = prompts.create_ui_plan(plan.task, project_dir)
        st.ui_plan = ui_plan
        state_module.save(st)
        print(f"[Boris] UI plan created with {len(ui_plan.milestones)} milestones", flush=True)
        logger.info("UI plan created with %d milestones", len(ui_plan.milestones))

    _run_ui_milestones(st, plan, project_dir, args, logger)


def _run_ui_milestones(st, plan, project_dir, args, logger):
    """Execute the UI milestone loop. Same pattern as structural but ships DaveLoop in UI test mode."""
    ui_plan = st.ui_plan
    if ui_plan is None:
        return

    for i, ui_milestone in enumerate(ui_plan.milestones):
        if ui_milestone.status == "completed":
            logger.info("Skipping already completed UI milestone %s", ui_milestone.id)
            continue
        if ui_milestone.status == "skipped":
            logger.info("Skipping previously skipped UI milestone %s", ui_milestone.id)
            continue

        print(flush=True)
        print(f"=== UI Milestone {ui_milestone.id}: {ui_milestone.title} ===", flush=True)
        logger.info("Starting UI milestone %s: %s", ui_milestone.id, ui_milestone.title)

        ui_milestone.status = "in_progress"
        state_module.save(st)

        # Craft prompt and ship DaveLoop
        print(f"[Boris] Crafting UI test prompt for {ui_milestone.id}...", flush=True)
        prompt = prompts.craft_ui_prompt(ui_milestone, ui_plan, plan, project_dir)
        print(f"[Boris] Prompt ready ({len(prompt)} chars). Spawning DaveLoop in UI Tester Mode...", flush=True)
        result = engine.run_ui_test(prompt, project_dir, args.max_iter, ui_milestone=ui_milestone)

        # Check verdict
        print(f"[Boris] DaveLoop finished. Checking UI verdict for {ui_milestone.id}...", flush=True)
        verdict_result = engine.check_ui(result, ui_milestone, ui_plan.test_tool)
        logger.info(
            "UI milestone %s verdict: %s - %s",
            ui_milestone.id, verdict_result.verdict.value, verdict_result.reason,
        )

        if verdict_result.verdict == Verdict.RESOLVED:
            ui_milestone.status = "completed"
            ui_milestone.completed_at = datetime.now().isoformat()
            state_module.save(st)

            if not st.no_git:
                # Lightweight commit for UI fixes
                ui_commit_ok = git_manager.commit_milestone(project_dir, state_module.Milestone(
                    id=ui_milestone.id, title=ui_milestone.title, description="",
                    depends_on=[], acceptance_criteria=[], files_to_create=[], files_to_modify=[],
                    status="completed",
                ))
                if ui_commit_ok:
                    logger.info("Git commit succeeded for UI milestone %s", ui_milestone.id)
                else:
                    logger.warning("Git commit FAILED for UI milestone %s", ui_milestone.id)
                    print(f"  [Boris] WARNING: Git commit failed for {ui_milestone.id}", flush=True)

            print(f"[Boris] UI Milestone {ui_milestone.id} COMPLETE", flush=True)
            if ui_milestone.issues_found:
                print(f"  [Boris] Issues found: {len(ui_milestone.issues_found)}", flush=True)
            if ui_milestone.issues_fixed:
                print(f"  [Boris] Issues fixed: {len(ui_milestone.issues_fixed)}", flush=True)
            logger.info("UI milestone %s completed", ui_milestone.id)

        elif verdict_result.verdict == Verdict.OFF_PLAN:
            correction_count = 0
            resolved = False

            while correction_count < MAX_CORRECTIONS:
                correction_count += 1
                print(
                    f"  [Boris] Off-plan detected. UI correction {correction_count}/{MAX_CORRECTIONS}...",
                    flush=True,
                )
                logger.info("Off-plan UI correction %d for %s", correction_count, ui_milestone.id)

                correction_prompt = prompts.craft_ui_correction(
                    result.output, ui_milestone, ui_plan, verdict_result.reason
                )
                result = engine.run_ui_test(correction_prompt, project_dir, args.max_iter, ui_milestone=ui_milestone)
                verdict_result = engine.check_ui(result, ui_milestone, ui_plan.test_tool)

                if verdict_result.verdict == Verdict.RESOLVED:
                    ui_milestone.status = "completed"
                    ui_milestone.completed_at = datetime.now().isoformat()
                    state_module.save(st)
                    print(f"[Boris] UI Milestone {ui_milestone.id} COMPLETE (after correction)", flush=True)
                    logger.info("UI milestone %s completed after correction", ui_milestone.id)
                    resolved = True
                    break

            if not resolved:
                print(f"  [Boris] UI Milestone {ui_milestone.id} could not be corrected. Skipping.", flush=True)
                logger.warning("UI milestone %s skipped after failed corrections", ui_milestone.id)
                ui_milestone.status = "skipped"
                state_module.save(st)

        elif verdict_result.verdict == Verdict.FAILED:
            retry_count = 0
            resolved = False

            while retry_count < MAX_RETRIES:
                retry_count += 1
                ui_milestone.retry_count = retry_count
                print(f"  [Boris] Failed. UI retry {retry_count}/{MAX_RETRIES}...", flush=True)
                logger.info("UI retry %d for %s", retry_count, ui_milestone.id)

                result = engine.run_ui_test(prompt, project_dir, args.max_iter, ui_milestone=ui_milestone)
                verdict_result = engine.check_ui(result, ui_milestone, ui_plan.test_tool)

                if verdict_result.verdict == Verdict.RESOLVED:
                    ui_milestone.status = "completed"
                    ui_milestone.completed_at = datetime.now().isoformat()
                    state_module.save(st)
                    print(f"[Boris] UI Milestone {ui_milestone.id} COMPLETE (after retry)", flush=True)
                    logger.info("UI milestone %s completed after retry", ui_milestone.id)
                    resolved = True
                    break

            if not resolved:
                print(f"  [Boris] UI Milestone {ui_milestone.id} failed after retries. Skipping.", flush=True)
                logger.warning("UI milestone %s skipped after failed retries", ui_milestone.id)
                ui_milestone.status = "skipped"
                state_module.save(st)

        state_module.save(st)

    # UI phase summary
    ui_completed = sum(1 for m in ui_plan.milestones if m.status == "completed")
    ui_total = len(ui_plan.milestones)
    print(flush=True)
    print(f"[Boris] UI Testing complete: {ui_completed}/{ui_total} milestones passed", flush=True)
    logger.info("UI testing complete: %d/%d", ui_completed, ui_total)


def main():
    """Main Boris orchestration loop."""
    args = parse_args()
    logger = setup_logging()

    # Create required dirs
    os.makedirs(PLANS_DIR, exist_ok=True)
    os.makedirs(LOGS_DIR, exist_ok=True)

    project_dir = os.path.abspath(args.dir)

    if args.resume:
        # Resume mode
        st = state_module.load(project_dir)
        if st is None:
            print("[Boris] Error: No saved state found. Cannot resume.", flush=True)
            sys.exit(1)
        plan = st.plan
        start_index = st.current_milestone_index

        # If already in UI testing phase, skip straight to UI loop
        if st.phase == "ui_testing":
            print(f"[Boris] Resuming UI Testing phase...", flush=True)
            logger.info("Resuming UI testing phase")
        else:
            print(f"[Boris] Resuming from milestone {start_index + 1}...", flush=True)
            logger.info("Resuming from milestone %d", start_index + 1)
    else:
        # New task mode
        if not args.task:
            print("[Boris] Error: task is required (unless using --resume)", flush=True)
            sys.exit(1)

        print_banner()
        logger.info("Starting Boris for task: %s", args.task)

        # Create plan (retry once on timeout)
        print("[Boris] Creating plan...", flush=True)
        plan = None
        for attempt in range(2):
            try:
                plan = prompts.create_plan(args.task, project_dir)
                break
            except RuntimeError as e:
                if attempt == 0 and "timed out" in str(e).lower():
                    print(f"  [Boris] Plan generation timed out. Retrying...", flush=True)
                    logger.warning("Plan timed out, retrying (attempt %d)", attempt + 2)
                else:
                    print(f"[Boris] Error creating plan: {e}", flush=True)
                    logger.error("Plan creation failed: %s", e)
                    sys.exit(1)
        if plan is None:
            print("[Boris] Error: Failed to create plan after retries.", flush=True)
            sys.exit(1)
        print(f"[Boris] Plan created with {len(plan.milestones)} milestones", flush=True)
        logger.info("Plan created with %d milestones", len(plan.milestones))

        print_plan_summary(plan)

        if args.plan_only:
            print("[Boris] Plan-only mode. Exiting.", flush=True)
            return

        # Create initial state
        st = state_module.State(
            plan=plan,
            current_milestone_index=0,
            project_dir=project_dir,
            git_remote=args.remote,
            no_git=args.no_git,
        )
        state_module.save(st)
        start_index = 0

    # Track start time for summary
    start_time = datetime.now()

    # Git setup
    if not st.no_git:
        print("[Boris] Initializing git repo...", flush=True)
        git_manager.init_repo(project_dir)
        print("[Boris] Verifying git config (user.name/email)...", flush=True)
        if git_manager.ensure_git_config(project_dir):
            logger.info("Git config verified")
        else:
            logger.warning("Git config verification had issues - commits may fail")
            print("  [Boris] WARNING: Git config issues detected. Commits may fail.", flush=True)
        if args.remote:
            print(f"[Boris] Setting up remote: {args.remote}", flush=True)
            git_manager.setup_remote(project_dir, args.remote)

    # Phase 1: Structural milestones (skip if resuming into UI phase)
    try:
        if st.phase != "ui_testing":
            for i in range(start_index, len(plan.milestones)):
                milestone = plan.milestones[i]

                if milestone.status == "completed":
                    logger.info("Skipping already completed milestone %s", milestone.id)
                    continue

                if milestone.status == "skipped":
                    logger.info("Skipping previously skipped milestone %s", milestone.id)
                    continue

                print(flush=True)
                print(f"=== Milestone {milestone.id}: {milestone.title} ===", flush=True)
                logger.info("Starting milestone %s: %s", milestone.id, milestone.title)

                # Mark in progress
                milestone.status = "in_progress"
                st.current_milestone_index = i
                state_module.save(st)

                # Craft prompt and execute
                print(f"[Boris] Crafting prompt for {milestone.id}...", flush=True)
                prompt = prompts.craft_prompt(milestone, plan, project_dir)
                print(f"[Boris] Prompt ready ({len(prompt)} chars). Spawning DaveLoop...", flush=True)
                result = engine.run(prompt, project_dir, args.max_iter, milestone=milestone)

                # Check verdict
                print(f"[Boris] DaveLoop finished. Checking verdict for {milestone.id}...", flush=True)
                verdict_result = engine.check(result, milestone)
                logger.info(
                    "Milestone %s verdict: %s - %s",
                    milestone.id,
                    verdict_result.verdict.value,
                    verdict_result.reason,
                )

                if verdict_result.verdict == Verdict.RESOLVED:
                    milestone.status = "completed"
                    milestone.completed_at = datetime.now().isoformat()
                    state_module.save(st)

                    if not st.no_git:
                        print(f"[Boris] Committing milestone {milestone.id} to git...", flush=True)
                        if git_manager.commit_milestone(project_dir, milestone):
                            logger.info("Git commit succeeded for milestone %s", milestone.id)
                        else:
                            logger.warning("Git commit FAILED for milestone %s", milestone.id)
                            print(f"  [Boris] WARNING: Git commit failed for {milestone.id}", flush=True)

                    print(f"[Boris] Milestone {milestone.id} COMPLETE", flush=True)
                    logger.info("Milestone %s completed", milestone.id)

                elif verdict_result.verdict == Verdict.OFF_PLAN:
                    # Attempt correction
                    correction_count = 0
                    resolved = False

                    while correction_count < MAX_CORRECTIONS:
                        correction_count += 1
                        print(
                            f"  [Boris] Off-plan detected. Correction attempt {correction_count}/{MAX_CORRECTIONS}...",
                            flush=True,
                        )
                        logger.info(
                            "Off-plan correction attempt %d for milestone %s",
                            correction_count,
                            milestone.id,
                        )

                        correction_prompt = prompts.craft_correction(
                            result.output, milestone, plan, verdict_result.reason
                        )
                        result = engine.run(correction_prompt, project_dir, args.max_iter, milestone=milestone)
                        verdict_result = engine.check(result, milestone)

                        if verdict_result.verdict == Verdict.RESOLVED:
                            milestone.status = "completed"
                            milestone.completed_at = datetime.now().isoformat()
                            state_module.save(st)

                            if not st.no_git:
                                print(f"[Boris] Committing corrected milestone {milestone.id} to git...", flush=True)
                                if git_manager.commit_milestone(project_dir, milestone):
                                    logger.info("Git commit succeeded for milestone %s (after correction)", milestone.id)
                                else:
                                    logger.warning("Git commit FAILED for milestone %s (after correction)", milestone.id)
                                    print(f"  [Boris] WARNING: Git commit failed for {milestone.id}", flush=True)

                            print(f"[Boris] Milestone {milestone.id} COMPLETE (after correction)", flush=True)
                            logger.info("Milestone %s completed after correction", milestone.id)
                            resolved = True
                            break

                    if not resolved:
                        print(f"  [Boris] Milestone {milestone.id} could not be corrected. Skipping.", flush=True)
                        logger.warning("Milestone %s skipped after failed corrections", milestone.id)
                        milestone.status = "skipped"
                        state_module.save(st)

                elif verdict_result.verdict == Verdict.FAILED:
                    # Retry logic
                    retry_count = 0
                    resolved = False

                    while retry_count < MAX_RETRIES:
                        retry_count += 1
                        milestone.retry_count = retry_count
                        print(
                            f"  [Boris] Failed. Retry {retry_count}/{MAX_RETRIES}...",
                            flush=True,
                        )
                        logger.info(
                            "Retry %d for milestone %s", retry_count, milestone.id
                        )

                        result = engine.run(prompt, project_dir, args.max_iter, milestone=milestone)
                        verdict_result = engine.check(result, milestone)

                        if verdict_result.verdict == Verdict.RESOLVED:
                            milestone.status = "completed"
                            milestone.completed_at = datetime.now().isoformat()
                            state_module.save(st)

                            if not st.no_git:
                                print(f"[Boris] Committing retried milestone {milestone.id} to git...", flush=True)
                                if git_manager.commit_milestone(project_dir, milestone):
                                    logger.info("Git commit succeeded for milestone %s (after retry)", milestone.id)
                                else:
                                    logger.warning("Git commit FAILED for milestone %s (after retry)", milestone.id)
                                    print(f"  [Boris] WARNING: Git commit failed for {milestone.id}", flush=True)

                            print(f"[Boris] Milestone {milestone.id} COMPLETE (after retry)", flush=True)
                            logger.info("Milestone %s completed after retry", milestone.id)
                            resolved = True
                            break

                    if not resolved:
                        print(
                            f"  [Boris] Milestone {milestone.id} failed after {MAX_RETRIES} retries. Skipping.",
                            flush=True,
                        )
                        logger.warning("Milestone %s skipped after failed retries", milestone.id)
                        milestone.status = "skipped"
                        state_module.save(st)

                # Save state after each milestone outcome
                state_module.save(st)

        # Phase 2: UI Testing & Polish
        _run_ui_phase(st, plan, project_dir, args, logger)

    except KeyboardInterrupt:
        print(flush=True)
        print(f"[Boris] Interrupted. State saved. Resume with: boris -r -d \"{project_dir}\"", flush=True)
        logger.info("Interrupted by user. State saved.")
        state_module.save(st)
        sys.exit(130)

    # Final push
    if not st.no_git:
        print("[Boris] Pushing final changes to remote...", flush=True)
        git_manager.final_push(project_dir)

    # Summary and proper exit
    print_final_summary(plan, project_dir, start_time)

    completed = sum(1 for m in plan.milestones if m.status == "completed")
    total = len(plan.milestones)
    if completed == total:
        sys.exit(0)
    else:
        sys.exit(1)


if __name__ == "__main__":
    main()
