import sys
from pathlib import Path
from typing import Optional

import yaml

from .core.about_car import (
    ensure_about_car_file_for_repo,
    ensure_destination_quickstart_file_for_repo,
    ensure_ticket_flow_quickstart_file_for_repo,
    ensure_tickets_agents_file_for_repo,
)
from .core.config import (
    CONFIG_FILENAME,
    DEFAULT_HUB_CONFIG,
    REPO_OVERRIDE_FILENAME,
    ConfigError,
    normalize_generated_hub_config,
    save_hub_config_data,
)
from .core.config import (
    GENERATED_CONFIG_HEADER as CORE_GENERATED_CONFIG_HEADER,
)
from .core.state import RunnerState, save_state
from .core.ticket_linter_cli import ensure_ticket_linter
from .core.ticket_manager_cli import ensure_ticket_manager
from .core.utils import atomic_write
from .manifest import load_manifest

GITIGNORE_CONTENT = "*"
GENERATED_CONFIG_HEADER = CORE_GENERATED_CONFIG_HEADER
HUB_CAR_SHIM_MARKER = "# CAR:HUB_CLI_SHIM"
HUB_CAR_SHIM_REL_PATH = Path(".codex-autorunner/bin/car")
HUB_CAR_SHIM_ROOT_BASENAME = "car"
PMA_DOCS_GENERATED_MARKER = "<!-- CAR:PMA_DOCS_GENERATED -->"
PMA_ALWAYS_REFRESH_DOCS = frozenset({"prompt.md", "ABOUT_CAR.md"})


def sample_todo() -> str:
    return ""


def sample_spec() -> str:
    return ""


def _seed_doc(path: Path, force: bool, content: str) -> None:
    if path.exists() and not force:
        return
    path.write_text(content, encoding="utf-8")


def _write_doc_if_changed(path: Path, content: str) -> None:
    try:
        existing = path.read_text(encoding="utf-8")
    except OSError:
        existing = None
    if existing == content:
        return
    atomic_write(path, content)


def pma_dir(hub_root: Path) -> Path:
    return hub_root / ".codex-autorunner" / "pma"


def pma_docs_dir(hub_root: Path) -> Path:
    return pma_dir(hub_root) / "docs"


def pma_doc_path(hub_root: Path, name: str) -> Path:
    return pma_docs_dir(hub_root) / name


def write_hub_config(hub_root: Path, force: bool = False) -> Path:
    config_path = hub_root / CONFIG_FILENAME
    if config_path.exists() and not force:
        try:
            data = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
        except yaml.YAMLError as exc:
            raise ConfigError(
                f"Invalid YAML in existing hub config {config_path}: {exc}. "
                "Back up the file or delete it, or rerun with --force to overwrite."
            ) from exc
        if not isinstance(data, dict):
            raise ConfigError(
                f"Existing hub config {config_path} must be a mapping. "
                "Back up the file or delete it, or rerun with --force to overwrite."
            )
        mode = data.get("mode")
        if mode != "hub":
            raise ConfigError(
                f"Existing config at {config_path} is not a hub config "
                f"(mode: {mode!r}). Move repo overrides into "
                f"{hub_root / REPO_OVERRIDE_FILENAME} or back up and delete the file, "
                "then rerun. Use --force to overwrite it."
            )
        normalize_generated_hub_config(config_path)
        return config_path
    config_path.parent.mkdir(parents=True, exist_ok=True)
    save_hub_config_data(
        config_path,
        {"version": DEFAULT_HUB_CONFIG["version"], "mode": "hub"},
        generated=True,
    )
    return config_path


def _build_hub_car_shim(python_executable: str) -> str:
    content = (
        "#!/bin/sh\n"
        f"{HUB_CAR_SHIM_MARKER}\n"
        "# Generated by CAR. Safe to overwrite.\n"
        f'exec "{python_executable}" -m codex_autorunner.cli "$@"\n'
    )
    return content


def _should_refresh_shim(path: Path, content: str, *, force: bool) -> bool:
    if force or not path.exists():
        return True
    try:
        existing = path.read_text(encoding="utf-8")
    except OSError:
        return True
    if HUB_CAR_SHIM_MARKER not in existing:
        return False
    return existing != content


def ensure_hub_car_shim(
    hub_root: Path,
    *,
    python_executable: Optional[str] = None,
    force: bool = False,
    include_root_shim: bool = True,
) -> list[Path]:
    """Ensure a hub-local car shim points at the current runtime."""
    python_executable = python_executable or sys.executable
    content = _build_hub_car_shim(python_executable)
    if content and not content.endswith("\n"):
        content += "\n"
    targets = [hub_root / HUB_CAR_SHIM_REL_PATH]
    if include_root_shim:
        targets.insert(0, hub_root / HUB_CAR_SHIM_ROOT_BASENAME)
    written: list[Path] = []
    for path in targets:
        if not _should_refresh_shim(path, content, force=force):
            continue
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text(content, encoding="utf-8")
        try:
            mode = path.stat().st_mode
            path.chmod(mode | 0o111)
        except OSError:
            pass
        written.append(path)
    return written


def seed_repo_files(
    repo_root: Path, force: bool = False, git_required: bool = True
) -> None:
    """
    Initialize a repository's .codex-autorunner directory with defaults.
    This is used by the CLI init path and hub auto-init discovery.
    Repo config is derived from the hub config; no repo config file is written.
    """
    if git_required and not (repo_root / ".git").exists():
        raise ValueError("Missing .git directory; pass git_required=False to bypass")

    ca_dir = repo_root / ".codex-autorunner"
    ca_dir.mkdir(parents=True, exist_ok=True)
    ensure_hub_car_shim(repo_root, force=force, include_root_shim=False)

    gitignore_path = ca_dir / ".gitignore"
    if not gitignore_path.exists() or force:
        gitignore_path.write_text(GITIGNORE_CONTENT, encoding="utf-8")

    state_path = ca_dir / "state.sqlite3"
    if not state_path.exists() or force:
        save_state(state_path, RunnerState(None, "idle", None, None, None))

    log_path = ca_dir / "codex-autorunner.log"
    if not log_path.exists() or force:
        log_path.write_text("", encoding="utf-8")

    tickets_dir = ca_dir / "tickets"
    if not tickets_dir.exists() or force:
        tickets_dir.mkdir(parents=True, exist_ok=True)

    contextspace_dir = ca_dir / "contextspace"
    if not contextspace_dir.exists() or force:
        contextspace_dir.mkdir(parents=True, exist_ok=True)

    _seed_doc(contextspace_dir / "active_context.md", force, sample_todo())
    _seed_doc(contextspace_dir / "decisions.md", force, "")
    _seed_doc(contextspace_dir / "spec.md", force, sample_spec())

    # Seed an always-available briefing doc for interactive Codex sessions.
    ensure_about_car_file_for_repo(
        repo_root,
        doc_paths={
            "active_context": contextspace_dir / "active_context.md",
            "decisions": contextspace_dir / "decisions.md",
            "spec": contextspace_dir / "spec.md",
        },
        force=force,
    )
    ensure_ticket_flow_quickstart_file_for_repo(repo_root, force=force)
    ensure_destination_quickstart_file_for_repo(repo_root, force=force)
    ensure_tickets_agents_file_for_repo(repo_root, force=force)
    ensure_ticket_linter(repo_root, force=force)
    ensure_ticket_manager(repo_root, force=force)


def ensure_pma_docs(hub_root: Path, force: bool = False) -> None:
    base_dir = pma_dir(hub_root)
    base_dir.mkdir(parents=True, exist_ok=True)
    docs_dir = pma_docs_dir(hub_root)
    docs_dir.mkdir(parents=True, exist_ok=True)
    seeds = (
        ("prompt.md", pma_prompt_content),
        ("ABOUT_CAR.md", pma_about_content),
        ("AGENTS.md", pma_agents_content),
        ("active_context.md", pma_active_context_content),
        ("context_log.md", pma_context_log_content),
    )
    for filename, content_fn in seeds:
        canonical_path = docs_dir / filename
        legacy_path = base_dir / filename
        canonical_existed = canonical_path.exists()
        content = content_fn()
        if content and not content.endswith("\n"):
            content += "\n"
        migrated_from_legacy = False

        # One-time migration from legacy PMA docs into canonical docs/.
        if legacy_path.exists():
            should_copy = False
            if filename not in PMA_ALWAYS_REFRESH_DOCS:
                should_copy = force
                if not force:
                    if not canonical_existed:
                        should_copy = True
                    else:
                        try:
                            should_copy = (
                                legacy_path.stat().st_mtime
                                > canonical_path.stat().st_mtime
                            )
                        except OSError:
                            should_copy = False
            if should_copy:
                try:
                    atomic_write(
                        canonical_path, legacy_path.read_text(encoding="utf-8")
                    )
                    migrated_from_legacy = True
                except OSError:
                    pass
            try:
                legacy_path.unlink()
            except OSError:
                pass

        if force or not canonical_path.exists():
            if (
                force
                and migrated_from_legacy
                and filename not in PMA_ALWAYS_REFRESH_DOCS
            ):
                # Preserve user-authored docs migrated from legacy paths.
                continue
            atomic_write(canonical_path, content)
            continue

        if filename in PMA_ALWAYS_REFRESH_DOCS:
            _write_doc_if_changed(canonical_path, content)


def seed_hub_files(hub_root: Path, force: bool = False) -> None:
    """
    Initialize a hub workspace with defaults and a manifest.
    """
    ca_dir = hub_root / ".codex-autorunner"
    ca_dir.mkdir(parents=True, exist_ok=True)

    gitignore_path = ca_dir / ".gitignore"
    if not gitignore_path.exists() or force:
        gitignore_path.write_text(GITIGNORE_CONTENT, encoding="utf-8")

    write_hub_config(hub_root, force=force)

    ensure_pma_docs(hub_root, force=force)
    ensure_hub_car_shim(hub_root, force=force)

    manifest_path = hub_root / DEFAULT_HUB_CONFIG["hub"]["manifest"]
    load_manifest(manifest_path, hub_root)

    hub_state_path = ca_dir / "hub_state.json"
    if not hub_state_path.exists() or force:
        atomic_write(
            hub_state_path,
            '{\n  "last_scan_at": null,\n  "repos": [],\n  "pinned_parent_repo_ids": []\n}\n',
        )


def pma_prompt_content() -> str:
    return (
        PMA_DOCS_GENERATED_MARKER
        + "\n"
        + """# Project Management Agent (PMA)

You are the hub-level Project Management Agent (PMA), the user's primary interface for coordinating work across repos.

## Role

You are an **abstraction layer, not an executor**. Coordinate tickets and flows across multiple repos by delegating to repo agents.

## Guidance

- Use CAR-native artifacts (tickets, ticket_flow, dispatch, PMA inbox/outbox).
- Ask questions when requirements are ambiguous; keep updates concise.
- Treat this prompt as code: keep it short and stable.
- See `.codex-autorunner/pma/docs/ABOUT_CAR.md` for operational how-to.

## Managed Threads vs Ticket Flows

- Use managed threads for exploratory/review/quick-fix/interactive debugging work in one repo.
- Use ticket flows for structured, multi-step deliverables with acceptance criteria or cross-repo changes.
- Managed thread state is visible in `hub_snapshot.pma_threads`.
- For hub-scoped PMA CLI commands, include `--path <hub_root>` so they resolve the
  intended hub config instead of relying on the current working directory.
- CLI primitives:
  - `car pma thread spawn --agent codex --repo <repo_id> --name <label> --path <hub_root>`
  - `car pma thread send --id <managed_thread_id> --message "..." --watch --path <hub_root>`
  - `car pma thread send --id <managed_thread_id> --message-file prompt.md --watch --path <hub_root>`
  - `car pma thread send --id <managed_thread_id> --message "..." --notify-on terminal --notify-lane <lane_id> --path <hub_root>`
  - `car pma thread status --id <managed_thread_id> --path <hub_root>`
  - `car pma thread compact --id <id> --summary "..." --path <hub_root>`
  - `car pma thread archive --id <id> --path <hub_root>`

## Automation primitives (event-driven continuity)

- Use automation when a lane should continue without manual polling.
- Subscriptions: run/thread transition triggers enqueue PMA wake-up turns.
  - `POST /hub/pma/subscriptions`
  - `GET /hub/pma/subscriptions`
  - `DELETE /hub/pma/subscriptions/{subscription_id}`
- Timers:
  - one-shot (`timer_type=one_shot`, `delay_seconds`)
  - watchdog (`timer_type=watchdog`, `idle_seconds`, optional `touch`)
  - `POST /hub/pma/timers`, `GET /hub/pma/timers`
  - `POST /hub/pma/timers/{timer_id}/touch`
  - `POST /hub/pma/timers/{timer_id}/cancel`
- Typical event_types:
  - ticket flow: `flow_paused`, `flow_completed`, `flow_failed`, `flow_stopped`
  - managed thread: `managed_thread_completed`, `managed_thread_failed`
- Durable state is stored at `.codex-autorunner/pma/automation_store.json`.
- Practical recipes are in `.codex-autorunner/pma/docs/ABOUT_CAR.md` under “PMA automation wake-ups”.

## Destinations (execution runtime)

- Repos/worktrees can run in `local` or `docker` destinations.
- Check destination before runtime-sensitive work:
  - `car hub destination show <repo_id> --path <hub_root>`
- Set destination when requested:
  - `car hub destination set <repo_id> local --path <hub_root>`
  - `car hub destination set <repo_id> docker --image <image> --path <hub_root>`
- For custom images and advanced docker options:
  - `car hub destination set --help`
  - Repo guide: `.codex-autorunner/DESTINATION_QUICKSTART.md`
  - Deep docs: `docs/configuration/destinations.md`, `docs/reference/hub-manifest-schema.md`
- For docker destinations, validate daemon connectivity and image availability before long runs.

## Ticket planning constraints

- Ticket flow executes `TICKET-###*.md` in ascending numeric order.
- It repeatedly selects the first ticket with `done != true`, runs that ticket, then advances.
- `depends_on` frontmatter is not supported; filename order is the only execution contract.
- If prerequisites are discovered late, split/reorder tickets so prerequisite work appears earlier.

## Ticket agent context (what each turn already gets)

- Current ticket file (`.codex-autorunner/tickets/TICKET-###*.md`).
- Pinned contextspace docs when present: `active_context.md`, `decisions.md`, `spec.md`.
- Prior human reply context and previous agent output when available.

## Worktrees (hub-managed)

- Prefer hub-owned worktrees:
  - Hub UI: “New Worktree”
  - CLI: `car hub worktree create <base_repo_id> <branch> [--start-point <ref>]`
  - Default start point is `origin/<default-branch>` unless `--start-point` is provided.
- If a worktree was created manually (e.g. `git worktree add`), it MUST be registered:
  - `car hub scan --path <hub_root>`
- Never copy `.codex-autorunner/` between worktrees. Each worktree has its own CAR state/docs.

## PMA durable workspace

Prefer writing durable guidance and recurring best-practices to `.codex-autorunner/pma/docs/AGENTS.md`.
Keep short-lived working context in `.codex-autorunner/pma/docs/active_context.md` and prune it when it grows.
When pruning, append the prior context to `.codex-autorunner/pma/docs/context_log.md` with a timestamp.
"""
    )


def pma_notes_content() -> str:
    return (
        PMA_DOCS_GENERATED_MARKER
        + "\n"
        + """# PMA Operations Guide

## Tickets (create/modify)

- Tickets live per repo at `<repo>/.codex-autorunner/tickets/`.
- Create or edit `TICKET-###*.md` files directly; keep diffs small and single-purpose.
- Set `done: true` in the ticket frontmatter only when the ticket is complete.

## Ticket flow (start/resume)

- Bootstrap (creates TICKET-001 if missing):
  `car flow ticket_flow bootstrap --repo <path>`
- Start/resume:
  `car flow ticket_flow start --repo <path>`
  `car flow ticket_flow start --repo <path>`
- Status/stop:
  `car flow ticket_flow status --repo <path> [--run-id <uuid>]`
  `car flow ticket_flow stop --repo <path> [--run-id <uuid>]`
- See `<repo>/.codex-autorunner/TICKET_FLOW_QUICKSTART.md` for CLI entrypoints + gotchas.

## Ticket flow mechanics (planning constraints)

- Ticket flow is an ordered queue, not a DAG scheduler.
- It chooses the first not-done ticket by number and runs one agent turn for that ticket.
- To avoid stuck runs, ensure prerequisites appear in earlier ticket numbers.
- If a ticket needs context from previous work, persist that context in `.codex-autorunner/contextspace/`.

## Ticket turn prompt context (what agents can read)

- The current ticket file (full markdown, including YAML frontmatter).
- Pinned contextspace docs when present: `active_context.md`, `decisions.md`, `spec.md`.
- Reply history from dispatch pauses and previous agent output when present.

## Worktrees 101 (Hub-managed)

Canonical worktree creation:
- Hub UI: “New Worktree”
- CLI (from hub root):
  `car hub worktree create <base_repo_id> <branch> [--start-point <ref>]`
  (defaults to `origin/<default-branch>` when `--start-point` is omitted)

Registering a manually-created worktree:
- If you used `git worktree add`, run:
  `car hub scan --path <hub_root>`
- Worktrees should live under the hub’s configured worktrees root and be shallow (depth=1) discoverable.

Naming / grouping:
- Prefer worktree directory names like:
  `<base_repo_id>--<branch>`
- This enables `worktree_of` inference during scan and grouping in the hub UI.

Do NOT copy `.codex-autorunner/` between worktrees:
- Each worktree is a full repo with its own `.codex-autorunner/` state/docs.
- Copying can introduce stale locks and confusing run metadata.

## Destinations (local/docker runtime)

- Show effective destination:
  `car hub destination show <repo_id> --path <hub_root>`
- Set destination to local:
  `car hub destination set <repo_id> local --path <hub_root>`
- Set destination to docker:
  `car hub destination set <repo_id> docker --image <image> --path <hub_root>`
- Bring your own image by passing any registry image ref to `--image`.
- Discover profile/workdir/env/mount flags:
  `car hub destination set --help`
- Quick guide:
  `<repo>/.codex-autorunner/DESTINATION_QUICKSTART.md`
- Deep docs:
  `docs/configuration/destinations.md` and `docs/reference/hub-manifest-schema.md`
- Docker `image` is required for docker destinations.
- If docker-backed runs fail, verify `docker --version` and `docker info`.

## Dispatch pauses (handle)

- Paused runs appear in the hub inbox snapshot with `repo_id`, `run_id`, and `open_url`.
- Read the latest dispatch, respond to the user, then resume the run.
- Dispatch history lives under:
  `<repo>/.codex-autorunner/runs/<run_id>/dispatch_history/####/DISPATCH.md`

## PMA file handoff

- User uploads arrive in `.codex-autorunner/filebox/inbox/`.
- Archive reviewed uploads with `car pma file consume|dismiss <filename> --path <hub_root>`.
- Restore archived uploads with `car pma file restore <filename> --path <hub_root>`.
- Send user-facing files by writing to `.codex-autorunner/filebox/outbox/`.

## PMA dispatches (user attention)

- Create PMA dispatches by writing Markdown files to:
  `.codex-autorunner/pma/dispatches/<timestamp>_<id>.md`
- File format: YAML frontmatter + markdown body.
- Required frontmatter fields:
  - `title`: short summary
  - `priority`: `info` | `warn` | `action`
  - `created_at`: ISO 8601 timestamp
  - `source_turn_id`: PMA turn id (for notifications)
  - `links`: optional list of `{label, href}` objects
- `resolved_at`: leave empty/omit until resolved
- The web UI lists unresolved dispatches; resolve via UI or set `resolved_at`.

## PMA hygiene (safe cleanup workflow)

- Preview categorized PMA cleanup candidates:
  `car pma hygiene --path <hub_root>`
- Limit to specific categories:
  `car pma hygiene --category files --category automation --path <hub_root>`
- Apply only the `safe` candidates after review:
  `car pma hygiene --apply --path <hub_root>`
- Categories are grouped as:
  - `safe`
  - `protected`
  - `needs-confirmation`
- Initial hygiene categories cover stale PMA inbox files, dormant managed-thread
  followups, automation artifacts, and PMA dispatch alerts.

## PMA automation wake-ups (subscriptions + timers)

Use automation to keep multi-lane work moving without explicit polling.

What to configure:
- Subscriptions:
  - trigger on lifecycle transitions and enqueue PMA wake-up turns.
  - create/list/delete:
    - `POST /hub/pma/subscriptions`
    - `GET /hub/pma/subscriptions`
    - `DELETE /hub/pma/subscriptions/{subscription_id}`
  - common `event_types`:
    - `flow_paused`, `flow_completed`, `flow_failed`, `flow_stopped`
    - `managed_thread_completed`, `managed_thread_failed`
- Timers:
  - one-shot (`timer_type=one_shot`, `delay_seconds`)
  - watchdog (`timer_type=watchdog`, `idle_seconds`)
  - create/list/touch/cancel:
    - `POST /hub/pma/timers`
    - `GET /hub/pma/timers`
    - `POST /hub/pma/timers/{timer_id}/touch`
    - `POST /hub/pma/timers/{timer_id}/cancel`

Example payloads:
```json
{
  "event_types": ["flow_completed"],
  "repo_id": "repo-1",
  "run_id": "run-1",
  "from_state": "running",
  "to_state": "completed",
  "lane_id": "pma:lane-next",
  "idempotency_key": "sub:repo-1:run-1:completed"
}
```

```json
{
  "timer_type": "watchdog",
  "idle_seconds": 300,
  "repo_id": "repo-1",
  "run_id": "run-1",
  "lane_id": "pma:default",
  "reason": "watchdog_stalled",
  "idempotency_key": "watchdog:repo-1:run-1"
}
```

Notes:
- Wake-up payloads include `repo_id`, `run_id`/`thread_id`, `from_state`, `to_state`, `reason`, `timestamp`, and `lane_id`.
- Handlers should be idempotent; duplicate events can occur by design.
- Durable storage path: `.codex-autorunner/pma/automation_store.json`.
"""
    )


def pma_about_content() -> str:
    return pma_notes_content()


def pma_agents_content() -> str:
    return """# PMA AGENTS (durable guidance + defaults)

This document is jointly maintained by the user and PMA.

## What belongs here

- Durable best-practices you want PMA to apply repeatedly ("defaults").
- Stable preferences (how to structure tickets, review habits, PR conventions).
- Template shortcuts / references that PMA should re-use.

## What does NOT belong here

- Temporary work-in-progress details (put those in `active_context.md`).
- Long transcripts.

## Template shortcuts (optional)

- Add references to frequently used ticket templates here (repo/path/ref) and when to apply them.

## CLI help-first rule

- Before using unfamiliar CAR CLI areas, run `car <group> --help`.
- Prefer the canonical commands shown in help/examples instead of ad-hoc shell flows.

## Defaults (examples)

- After implementation work, add a final review ticket, then a ticket to open a PR.
"""


def pma_active_context_content() -> str:
    return """# PMA active context (short-lived)

Use this file for the current working set: active projects, open questions, links, and immediate next steps.

Pruning guidance:
- Keep this file compact (prefer bullet points).
- When it grows too large, summarize older items and move durable guidance to `AGENTS.md`.
- Before a major prune, append a timestamped snapshot to `context_log.md`.
"""


def pma_context_log_content() -> str:
    return """# PMA context log (append-only)

This file is an append-only history of past `active_context.md` snapshots.

- Add a new section with an ISO timestamp when you perform a major prune.
- Keep entries concise; the goal is searchability and historical recall.
"""
