"""Auto-memory commit helpers used by close_session (#161).

Writes a project-state snapshot to Claude Code's per-project auto-memory
directory at session close so the next session boots with current
project context already in Claude's prompt — no GitHub round-trip
needed to re-orient. Sibling to #157 (always-engage on session start);
together they form the always-engaged lifecycle contract.

Path convention: `~/.claude/projects/<sanitized-cwd>/memory/`, where
`<sanitized-cwd>` is the absolute project path with `/` replaced by
`-`. Detection is conservative: if the directory does not exist (e.g.
auto-memory disabled, harness too old), the write is skipped silently
— the GitHub-side session-close still happens.

Snapshot scope is durable, project-level state — active milestones,
repo shape, decisions made this session — explicitly NOT ephemeral
state ("in-progress work, current conversation context") which the
auto-memory protocol forbids.
"""
from __future__ import annotations

import os
from pathlib import Path

from .models import GithubConfig
from .utils import SessionActivity, compose_session_summary

_MEMORY_FILENAME = "project_session_state.md"
_MEMORY_INDEX = "MEMORY.md"


def compute_memory_dir(cwd: str | None = None) -> Path:
    """Derive Claude Code's per-project auto-memory directory path.

    Convention: `~/.claude/projects/<sanitized-cwd>/memory/` where
    `<sanitized-cwd>` is the absolute path with `/` replaced by `-`.
    Tilde expansion uses the current user's HOME.

    Pass `cwd` explicitly in tests; defaults to `os.getcwd()`.
    """
    cwd_path = cwd if cwd is not None else os.getcwd()
    sanitized = cwd_path.replace("/", "-")
    return Path.home() / ".claude" / "projects" / sanitized / "memory"


def compose_session_state_memory(
    config: GithubConfig,
    activity: SessionActivity,
    skill_update_candidates: int,
    has_pending_buffer: bool,
) -> str:
    """Render the auto-memory file body for session close (#161).

    Pure function — deterministic, no IO. Output respects the
    auto-memory `project`-type schema (frontmatter + Why/How-to-apply
    body). Snapshot is what's durable: active repo pair, related-repo
    graph, and the same activity counters used by the GitHub-side
    session summary. LLM-driven distillation of decisions/blockers is
    intentionally deferred — keeps tests deterministic.
    """
    summary_line = compose_session_summary(
        activity, skill_update_candidates, has_pending_buffer
    )
    related = config.related_repos
    related_lines = (
        "\n".join(
            f"- {r.repo} ({r.relationship}){': ' + r.description if r.description else ''}"
            for r in related
        )
        if related
        else "_None._"
    )
    body = (
        "---\n"
        "name: Project session state — active-claude-github\n"
        "description: Snapshot of repo pair, related-repo graph, and "
        "last-session activity counters; refreshed at every "
        "close_session call. Verify against current state before acting "
        "on it — memory records can drift.\n"
        "type: project\n"
        "---\n\n"
        "Last close_session activity (replaced each close):\n\n"
        f"- {summary_line}\n\n"
        "**Why:** the durable shape of this project (repo pair, "
        "related repos, account type) and the most-recent session "
        "activity are the minimum-viable context for the next session "
        "to re-orient without re-fetching from GitHub.\n\n"
        "**How to apply:** treat as cached state at session start. "
        "Verify against `git log` / Project TOC before relying on any "
        "specific count. Do not extrapolate from this snapshot beyond "
        "its named scope (no in-progress task list, no decisions, no "
        "blockers — those are not derivable from activity counters).\n\n"
        "## Repos\n"
        f"- Public: {config.repos.public}\n"
        f"- Private (dev): {config.repos.private}\n"
        f"- Account type: {config.account_type}\n\n"
        "## Related repos\n"
        f"{related_lines}\n"
    )
    return body


def _ensure_memory_index_pointer(index_path: Path, file_relname: str) -> None:
    """If MEMORY.md exists, ensure a one-line pointer at `file_relname`.

    Adds an entry only if no line already mentions the filename. Leaves
    the rest of MEMORY.md untouched — the index is a user artifact and
    we don't reorder or rewrite it.
    """
    if not index_path.exists():
        return
    try:
        existing = index_path.read_text(encoding="utf-8")
    except OSError:
        return
    if file_relname in existing:
        return
    pointer = (
        f"- [Project session state]({file_relname}) — repo pair + "
        "last-session activity, refreshed by close_session"
    )
    new = existing.rstrip() + "\n" + pointer + "\n"
    try:
        index_path.write_text(new, encoding="utf-8")
    except OSError:
        # Index update is best-effort; don't fail the close on it.
        return


def commit_session_state(
    config: GithubConfig,
    activity: SessionActivity,
    skill_update_candidates: int,
    has_pending_buffer: bool,
    *,
    cwd: str | None = None,
    memory_dir: Path | None = None,
) -> dict:
    """Write the project-state snapshot to auto-memory; orchestrate the close-side write.

    Returns a status dict:

      {"committed": True,  "path": "/abs/path/to/file"}                 # success
      {"committed": False, "skipped_reason": "memory dir does not exist"}
      {"committed": False, "skipped_reason": "write failed: <details>"}

    `memory_dir` overrides path detection; useful in tests. `cwd`
    overrides `os.getcwd()` when computing the default memory dir.
    """
    target = memory_dir if memory_dir is not None else compute_memory_dir(cwd)
    if not target.exists():
        return {
            "committed": False,
            "skipped_reason": "memory directory does not exist",
        }
    if not target.is_dir():
        return {
            "committed": False,
            "skipped_reason": f"memory path is not a directory: {target}",
        }

    body = compose_session_state_memory(
        config, activity, skill_update_candidates, has_pending_buffer
    )
    out_path = target / _MEMORY_FILENAME
    try:
        out_path.write_text(body, encoding="utf-8")
    except OSError as exc:
        return {
            "committed": False,
            "skipped_reason": f"write failed: {exc}",
        }

    _ensure_memory_index_pointer(target / _MEMORY_INDEX, _MEMORY_FILENAME)

    return {"committed": True, "path": str(out_path)}
