"""Session and actor scaffolding helpers."""

from __future__ import annotations

import os
from pathlib import Path
import shutil
import subprocess
from textwrap import dedent

from gotta.compat import UTC, datetime
from gotta.content.env import (
    CONTENT_ENV,
    SESSION_ACTOR_ENV,
    SESSION_CREATED_ENV,
    SESSION_ENV,
    SESSION_INITIALIZED_ENV,
    SESSION_REPO_ENV,
    env_mapping,
    load_state_env_at_root,
    state_env_path,
    write_session_state,
)
from gotta.content.file import ensure_private_dir, write_text_atomic
from gotta.content.model import CommonOptions
from gotta.content.path import sh_quote
from gotta.content.scope import resolve_dirs, session_surface_initialized
from gotta.logs import append_log_record, logs_state_path
from gotta.actor import (
    actor_session_root,
)
from gotta.todo import (
    create_todo_item,
    todo_items,
    todo_state_path,
)

from .activity import _record_session_activity
from .registry import (
    SESSION_ACTORS_ENV,
    SESSION_ACTORS_SOURCE_ENV,
    SESSION_VOICE_SOURCE_ENV,
    SESSION_WANT_PATH_ENV,
    WANT_FILE,
    _actor_charter_command,
    _actor_is_selected,
    _actor_label,
    _actor_registry,
    _actor_registry_json,
    _group_session_root,
    _normalize_actor_name,
    _read_actor_state,
    _resolve_bound_actor_name,
    _write_actor_state,
)

SESSION_STATE_KEYS = (
    SESSION_INITIALIZED_ENV,
    SESSION_CREATED_ENV,
    SESSION_ACTORS_ENV,
    SESSION_WANT_PATH_ENV,
    SESSION_ACTORS_SOURCE_ENV,
    SESSION_VOICE_SOURCE_ENV,
    SESSION_ACTOR_ENV,
)


def _finish(text: str) -> str:
    return dedent(text).strip() + "\n"


def _bootstrap_want() -> str:
    return _finish(
        """
        # Want

        > Generated by gotta session bootstrap.
        > This is a rewrite-on-purpose intent frame, not an append surface.

        This file carries the current expression of what the operator wants.

        Rewrite it intentionally from live context. Keep it current as the desired
        outcome, pressure, and deliverable sharpen.

        ## Want

        _empty_
        """
    )


def _bootstrap_actor_want(*, actor_name: str, label: str) -> str:
    actor_name = _normalize_actor_name(actor_name)
    return _finish(
        f"""
        # Actor Want Placeholder

        > Generated by `gotta actor bind {label}`.
        > Rewrite this file before actor launch.

        This is the operator-authored actor-local intent frame for {label}.

        It is a live charter, not an append surface and not a hidden template.
        Rewrite it intentionally so the actor starts from a real contract instead of
        trying to infer one from the shared session.

        Native rewrite:

        - `{_actor_charter_command(actor_name, "want")}`
        - `{_actor_charter_command(actor_name, "want", mode="--from-file <path>")}`
        """
    )


def _bootstrap_goal(
    *,
    repo: Path | None,
    work_dir: Path,
    agents_src: Path | None,
    voice_src: Path | None,
) -> str:
    markers = "".join(
        line
        for line in (
            f"- Repository: `{repo}`\n" if repo is not None else "",
            "- AGENTS.md present in this directory\n" if agents_src else "",
            "- VOICE.md present in this directory\n" if voice_src else "",
        )
    )
    return _finish(
        f"""
        # Seed Goal Placeholder

        > Generated by gotta session bootstrap.
        > This is an intentional rewrite target, not an append log.

        This file is a rewrite-on-purpose execution charter. Rewrite it from live
        context before launching actors or treating it as current truth.

        Bootstrap facts:

        - Operating directory: `{work_dir}`
        - Durable intent frame: `WANT.md`
        - Canonical coordination state: `state/todo.jsonl`
        - Supporting canonical logs: `state/logs.jsonl`, `state/oops.jsonl`, `state/env`
        - Live readable surfaces: `gotta todo`, `gotta logs`, `gotta oops`
        {markers}
        GOAL.md rule:

        - Read `WANT.md` first.
        - Read `gotta todo`, `gotta logs`, and `gotta oops`.
        - Treat `state/todo.jsonl`, `state/logs.jsonl`, and `state/oops.jsonl` as the
          canonical continuous state behind those CLI surfaces.
        - Provider-native `search` commands can seed discovery artifacts and
          provider-native `get` / plain `gotta read <locator>` retrieval can land
          evidence artifacts, but only when an initialized session is already
          active or passed explicitly. Sessionless retrieval still works; it just
          does not store artifacts.
        - Do not worry if `session manifest`, `session leads`, `session graph`, or
          `session analyze` are empty before the first stored retrieval; land one
          strong source anchor first, then use those session surfaces to continue.
          `session leads` shows the current best leads first and keeps the
          provenance signals visible so you can choose between materialized,
          native, search-seed, and otherwise promising unmaterialized branches.
        - Rewrite `GOAL.md` in place so it reflects the current moment.
        - Keep `GOAL.md` durable for actor launches and reruns.
        - Launch actors once `WANT.md` and `GOAL.md` are real.
        """
    )


def _bootstrap_actor_goal(
    *, actor_name: str, label: str, actor_dir: Path, work_dir: Path
) -> str:
    actor_name = _normalize_actor_name(actor_name)
    return _finish(
        f"""
        # Seed Actor Goal Placeholder

        > Generated by `gotta actor bind {label}`.
        > Rewrite this file before actor launch.

        This file turns the operator-authored actor `WANT.md` into a concrete
        evidence-collection contract for {label}.

        Bootstrap facts:

        - Actor session root: `{actor_dir}`
        - Actor-local authored charters: `{actor_dir / WANT_FILE}`, `{actor_dir / "GOAL.md"}`
        - Actor-local live surfaces: `gotta todo`, `gotta notes`, `gotta logs`, `gotta oops`

        Rewrite rule:

        - Read actor-local `WANT.md` first.
        - Turn that charter into concrete evidence-collection steps.
        - Treat `gotta todo` as the live actor-local checklist.
        - Treat `gotta notes` as the canonical actor-authored narration surface.
        - Treat `gotta logs` as procedural/system trace rather than the primary narration surface.
        - Append an initial short heartbeat note immediately after launch, even before the first evidence wave.
        - Continue appending short notes during the run; do not wait until the end.
        - Do not author the final dossier from this session.

        Native rewrite:

        - `{_actor_charter_command(actor_name, "goal")}`
        - `{_actor_charter_command(actor_name, "goal", mode="--from-file <path>")}`
        """
    )


def _seed_file(path: Path, content: str) -> None:
    if not path.exists():
        write_text_atomic(path, content)


def _seed_actor_surface(
    actor_dir: Path,
    actor_name: str,
    label: str,
    *,
    work_dir: Path,
) -> None:
    ensure_private_dir(actor_dir)
    _seed_file(
        actor_dir / WANT_FILE,
        _bootstrap_actor_want(actor_name=actor_name, label=label),
    )
    _seed_file(
        actor_dir / "GOAL.md",
        _bootstrap_actor_goal(
            actor_name=actor_name,
            label=label,
            actor_dir=actor_dir,
            work_dir=work_dir,
        ),
    )


def _reset_orphaned_actor_surface(actor_dir: Path) -> None:
    managed_paths = (
        actor_dir / WANT_FILE,
        actor_dir / "GOAL.md",
        actor_dir / "content",
        actor_dir / "session",
    )
    for path in managed_paths:
        if path.is_symlink() or path.is_file():
            path.unlink(missing_ok=True)
        elif path.exists():
            shutil.rmtree(path)


def _load_session_state(work_dir: Path) -> dict[str, str]:
    return load_state_env_at_root(work_dir)


def _ensure_symlink(path: Path, target: Path) -> None:
    desired = os.path.relpath(target, start=path.parent)
    if path.is_symlink():
        current = os.readlink(path)
        if current == desired:
            return
        path.unlink()
    elif path.exists():
        if path.is_file():
            path.unlink()
        else:
            return
    ensure_private_dir(path.parent)
    try:
        path.symlink_to(desired)
    except FileExistsError:
        if path.is_symlink():
            current = os.readlink(path)
            if current == desired:
                return
            path.unlink()
        elif path.exists() and path.is_file():
            path.unlink()
        else:
            raise
        path.symlink_to(desired)


def _find_upward_file(start: Path, name: str) -> Path | None:
    for directory in (start.resolve(), *start.resolve().parents):
        candidate = directory / name
        if candidate.is_file():
            return candidate
    return None


def _copy_if_present(source: Path | None, destination: Path) -> None:
    if source is None or not source.is_file():
        return
    ensure_private_dir(destination.parent)
    shutil.copy2(source, destination)


def _default_repo() -> Path:
    result = subprocess.run(
        ["git", "rev-parse", "--show-toplevel"],
        check=True,
        capture_output=True,
        text=True,
    )
    return Path(result.stdout.strip())


def _ensure_state_exports(work_dir: Path) -> dict[str, str]:
    dirs = resolve_dirs(CommonOptions(session_dir=str(work_dir)), create=True)
    existing = load_state_env_at_root(work_dir)
    preserved = {
        key: value
        for key, value in existing.items()
        if key not in {SESSION_ENV, CONTENT_ENV, SESSION_REPO_ENV}
    }
    write_session_state(dirs, preserved)
    return env_mapping(dirs)


def _write_state_file(work_dir: Path, values: dict[str, str]) -> None:
    existing = dict(load_state_env_at_root(work_dir).items())
    base_exports = _ensure_state_exports(work_dir)
    merged = {
        **existing,
        **base_exports,
        **values,
    }
    ordered_keys = [SESSION_ENV, CONTENT_ENV, SESSION_REPO_ENV, *SESSION_STATE_KEYS]
    extras = [key for key in merged if key not in ordered_keys]
    lines: list[str] = []
    for key in ordered_keys + sorted(extras):
        value = merged.get(key)
        if value is None:
            continue
        lines.append(f"export {key}={sh_quote(str(value))}")
    path = state_env_path(work_dir)
    ensure_private_dir(path.parent)
    text = "\n".join(lines) + "\n"
    if path.exists():
        try:
            if path.read_text(encoding="utf-8") == text:
                return
        except OSError:
            pass
    write_text_atomic(path, text)


def _seed_session_files(
    *,
    session_dir: Path,
    repo: Path | None,
    actors: dict[str, dict[str, str]],
    agents_src: Path | None,
    voice_src: Path | None,
) -> None:
    _copy_if_present(agents_src, session_dir / "AGENTS.md")
    _copy_if_present(voice_src, session_dir / "VOICE.md")
    _seed_file(session_dir / WANT_FILE, _bootstrap_want())
    _seed_file(
        session_dir / "GOAL.md",
        _bootstrap_goal(
            repo=repo,
            work_dir=session_dir,
            agents_src=agents_src,
            voice_src=voice_src,
        ),
    )
    if not todo_state_path(session_dir).exists():
        create_todo_item(
            session_dir,
            section="Status",
            text=f"Session root established at `{session_dir}`",
            checked=True,
        )
        create_todo_item(
            session_dir,
            section="Status",
            text=f"{WANT_FILE} rewritten by the active actor from live context",
        )
        create_todo_item(
            session_dir,
            section="Status",
            text="GOAL.md rewritten by the active actor from live context",
        )
        create_todo_item(
            session_dir,
            section="Status",
            text="Expand the canonical checklist through `gotta todo` until it reflects real working state.",
        )
        create_todo_item(
            session_dir,
            section="Status",
            text="Append a first short note as soon as the main actor lands a strong anchor, then append another short note after the first substantive evidence wave and before final synthesis/signoff.",
        )
        create_todo_item(
            session_dir,
            section="Status",
            text=(
                "If a sibling actor will help, bind them first with `gotta actor bind ...`; "
                "actor-targeted `gotta want|goal --actor ...` rewrites only become addressable "
                "after that bind completes, then actually consult them if it helps."
            ),
        )
    if not logs_state_path(session_dir).exists():
        bootstrap_lines = [
            "Session surface initialized by gotta session bootstrap",
            f"Session root: `{session_dir}`",
            "Actors:",
        ]
        if repo is not None:
            bootstrap_lines.insert(1, f"Repository: `{repo}`")
        for actor_id, payload in actors.items():
            bootstrap_lines.append(
                f"  - {str(payload.get('label') or _actor_label(actor_id))}: model = `{payload.get('model', '')}`, resume = `{payload.get('resume_uuid', '')}`"
            )
        if agents_src:
            bootstrap_lines.append("Imported `AGENTS.md`")
        if voice_src:
            bootstrap_lines.append("Imported `VOICE.md`")
        bootstrap_lines.append("Seeded as the procedural/system trace for this session")
        bootstrap_lines.append(
            "Keep actor-authored narration in `gotta notes`; one-line notes after strong anchors, substantive evidence waves, and final synthesis/signoff are expected."
        )
        append_log_record(session_dir, message="\n".join(bootstrap_lines))


def scaffold_session(session_dir: Path, *, repo: Path | None = None) -> None:
    current = _load_session_state(session_dir)
    existing_repo = str(current.get(SESSION_REPO_ENV) or "").strip()
    repo_path: Path | None
    if repo is not None:
        repo_path = repo.expanduser().resolve()
    elif existing_repo:
        repo_path = Path(existing_repo).expanduser().resolve()
    else:
        try:
            repo_path = _default_repo().resolve()
        except (subprocess.CalledProcessError, FileNotFoundError):
            repo_path = None
    actors = _actor_registry(session_dir)
    created_at = current.get(SESSION_CREATED_ENV) or datetime.now(tz=UTC).strftime(
        "%Y-%m-%dT%H:%M:%SZ"
    )
    home = Path.home()
    agents_src = _find_upward_file(repo_path, "AGENTS.md") if repo_path else None
    voice_src = _find_upward_file(repo_path, "VOICE.md") if repo_path else None
    if agents_src is None and (home / "AGENTS.md").is_file():
        agents_src = home / "AGENTS.md"
    if voice_src is None and (home / "VOICE.md").is_file():
        voice_src = home / "VOICE.md"
    _write_state_file(
        session_dir,
        {
            SESSION_REPO_ENV: str(repo_path or ""),
            SESSION_INITIALIZED_ENV: "1",
            SESSION_CREATED_ENV: created_at,
            SESSION_ACTORS_ENV: _actor_registry_json(actors),
            SESSION_WANT_PATH_ENV: str(session_dir / WANT_FILE),
            SESSION_ACTORS_SOURCE_ENV: str(agents_src or ""),
            SESSION_VOICE_SOURCE_ENV: str(voice_src or ""),
        },
    )
    _seed_session_files(
        session_dir=session_dir,
        repo=repo_path,
        actors=actors,
        agents_src=agents_src,
        voice_src=voice_src,
    )
    _record_session_activity(
        session_dir,
        plugin="session",
        surface="session.init",
        action="init",
        target=session_dir / WANT_FILE,
        detail="scaffolded WANT.md and GOAL.md; canonical live surfaces are now CLI-native",
    )


def _ensure_actor_session_exports(
    actor_dir: Path,
    *,
    content_dir: Path,
) -> dict[str, str]:
    dirs = resolve_dirs(
        CommonOptions(session_dir=str(actor_dir), content_dir=str(content_dir)),
        create=True,
    )
    write_session_state(dirs)
    _ensure_symlink(actor_dir / "content", content_dir)
    session_link = actor_dir / "session"
    if session_link.is_symlink() or session_link.is_file():
        session_link.unlink(missing_ok=True)
    return env_mapping(dirs)


def _ensure_actor_parent_links(
    session_root: Path, actor_name: str, actor_dir: Path
) -> None:
    return None


def _ensure_actor_initial_todo(actor_dir: Path) -> None:
    if todo_items(actor_dir):
        return
    create_todo_item(
        actor_dir,
        section="Status",
        text="Rewrite WANT.md and GOAL.md into a concrete actor-local checklist through `gotta todo`.",
    )
    create_todo_item(
        actor_dir,
        section="Status",
        text="Append an initial short heartbeat note immediately after the actor is alive, even before the first evidence wave.",
    )
    create_todo_item(
        actor_dir,
        section="Status",
        text="Materialize the first strong source anchor and append a first short note as soon as it lands.",
    )
    create_todo_item(
        actor_dir,
        section="Status",
        text="Append another short note after each material evidence wave or plan change; do not request completion or sign-off with empty actor notes.",
    )


def _ensure_actor_surface(work_dir: Path, actor_name: str) -> Path:
    actor_name = _resolve_bound_actor_name(work_dir, actor_name)
    state = _load_session_state(work_dir)
    actors = _actor_registry(work_dir)
    actor = actors.get(actor_name)
    if actor is None:
        raise SystemExit(f"unknown actor: {actor_name}")
    repo_raw = str(state.get(SESSION_REPO_ENV) or "").strip()
    actor_dir = actor_session_root(work_dir, actor_name)
    shared_content_dir = _group_session_root(work_dir) / "content"
    if actor_dir != work_dir:
        if actor_dir.exists() and not _actor_is_selected(work_dir, actor_name):
            _reset_orphaned_actor_surface(actor_dir)
        _seed_actor_surface(
            actor_dir,
            actor_name,
            _actor_label(actor_name, work_dir=work_dir),
            work_dir=work_dir,
        )
        _copy_if_present(work_dir / "AGENTS.md", actor_dir / "AGENTS.md")
        _copy_if_present(work_dir / "VOICE.md", actor_dir / "VOICE.md")
        _ensure_actor_session_exports(
            actor_dir,
            content_dir=shared_content_dir,
        )
        _ensure_actor_parent_links(work_dir, actor_name, actor_dir)
    else:
        _ensure_state_exports(actor_dir)
    _write_state_file(
        actor_dir,
        {
            SESSION_INITIALIZED_ENV: "1",
            SESSION_REPO_ENV: repo_raw,
            SESSION_CREATED_ENV: str(state.get(SESSION_CREATED_ENV) or ""),
            SESSION_ACTORS_ENV: _actor_registry_json(actors),
            SESSION_WANT_PATH_ENV: str(actor_dir / WANT_FILE),
            SESSION_ACTORS_SOURCE_ENV: str(state.get(SESSION_ACTORS_SOURCE_ENV) or ""),
            SESSION_VOICE_SOURCE_ENV: str(state.get(SESSION_VOICE_SOURCE_ENV) or ""),
            SESSION_ACTOR_ENV: actor_name,
        },
    )
    if not session_surface_initialized(actor_dir):
        repo_path = Path(repo_raw).expanduser().resolve() if repo_raw else None
        scaffold_session(actor_dir, repo=repo_path)
    _ensure_actor_initial_todo(actor_dir)
    _write_actor_state(
        work_dir,
        actor_name,
        {
            "status": str(
                _read_actor_state(work_dir, actor_name).get("status") or "pending"
            ),
        },
    )
    return actor_dir
