"""context_export — Deterministic context bundle generator.

Assembles a portable ``context_bundle/`` snapshot from:

1. ``memory/`` deployed files (excluding templates/, archive/, and README.md)
2. ``context/current/`` agent-bootstrap context files

Outputs:

* ``context_bundle/BUNDLE.md``   — human-readable summary of bundle contents
* ``context_bundle/pointers.json`` — machine-readable file index with SHA-256 digests

Determinism guarantee:
    Identical file inputs produce byte-identical outputs.
    No timestamps, no random elements, no system-dependent fields.
    File list is sorted lexicographically. JSON keys are sorted.

Usage::

    from context_export import export_bundle, ExportResult
    result = export_bundle(repo_root=Path("."))
    # result.bundle_md_path, result.pointers_json_path, result.file_count

CLI: see ``script/context-export`` (bash shim → delegates here via
``python script/_lib/context_export.py``).
"""
from __future__ import annotations

import hashlib
import json
import sys
from dataclasses import dataclass, field
from pathlib import Path

# ---------------------------------------------------------------------------
# Public types
# ---------------------------------------------------------------------------


@dataclass
class FileEntry:
    """A single file included in the context bundle."""

    path: str          # Relative path from repo root, forward-slash separated
    sha256: str        # Hex digest
    source: str        # "memory" | "context"
    size_bytes: int


@dataclass
class ExportResult:
    """Result of a successful (or dry-run) export operation."""

    file_count: int
    bundle_hash: str           # SHA-256 over sorted file digests (content hash of the bundle)
    entries: list[FileEntry] = field(default_factory=list)
    bundle_md_path: Path | None = None
    pointers_json_path: Path | None = None
    dry_run: bool = False


# ---------------------------------------------------------------------------
# Internals
# ---------------------------------------------------------------------------

_MEMORY_EXCLUDES = {"templates", "archive"}


def _sha256_file(path: Path) -> str:
    h = hashlib.sha256()
    h.update(path.read_bytes())
    return h.hexdigest()


def _bundle_hash(entries: list[FileEntry]) -> str:
    """Deterministic hash over all file digests, sorted by path."""
    h = hashlib.sha256()
    for e in sorted(entries, key=lambda e: e.path):
        h.update(e.sha256.encode())
    return h.hexdigest()


def _collect_memory_files(memory_dir: Path, repo_root: Path) -> list[FileEntry]:
    if not memory_dir.is_dir():
        return []
    entries: list[FileEntry] = []
    for p in sorted(memory_dir.rglob("*.md")):
        # Exclude templates/, archive/, and README.md at memory root
        parts = p.relative_to(memory_dir).parts
        if parts[0] in _MEMORY_EXCLUDES:
            continue
        if p.name == "README.md" and p.parent == memory_dir:
            continue
        entries.append(FileEntry(
            path=p.relative_to(repo_root).as_posix(),
            sha256=_sha256_file(p),
            source="memory",
            size_bytes=p.stat().st_size,
        ))
    return entries


def _collect_context_files(context_current_dir: Path, repo_root: Path) -> list[FileEntry]:
    if not context_current_dir.is_dir():
        return []
    entries: list[FileEntry] = []
    for p in sorted(context_current_dir.rglob("*")):
        if not p.is_file():
            continue
        entries.append(FileEntry(
            path=p.relative_to(repo_root).as_posix(),
            sha256=_sha256_file(p),
            source="context",
            size_bytes=p.stat().st_size,
        ))
    return entries


def _build_bundle_md(entries: list[FileEntry], bundle_hash: str) -> str:
    """Build the BUNDLE.md content (deterministic, no timestamps)."""
    memory_entries = sorted([e for e in entries if e.source == "memory"], key=lambda e: e.path)
    context_entries = sorted([e for e in entries if e.source == "context"], key=lambda e: e.path)

    lines: list[str] = [
        "# Context Bundle",
        "",
        "Portable session context assembled by `script/context-export`.",
        "This file is deterministic: identical inputs produce identical output.",
        "",
        f"**Bundle hash:** `{bundle_hash[:16]}...`",
        f"**Files:** {len(entries)} total "
        f"({len(memory_entries)} memory, {len(context_entries)} context)",
        "",
    ]

    if memory_entries:
        lines += ["## Memory Files", ""]
        for e in memory_entries:
            lines.append(f"- `{e.path}` ({e.size_bytes} bytes, sha256: `{e.sha256[:12]}...`)")
        lines.append("")

    if context_entries:
        lines += ["## Agent-Bootstrap Context Files", ""]
        for e in context_entries:
            lines.append(f"- `{e.path}` ({e.size_bytes} bytes, sha256: `{e.sha256[:12]}...`)")
        lines.append("")

    lines += [
        "## How to Use",
        "",
        "Point an AI agent at this directory. The agent should read:",
        "1. `BUNDLE.md` (this file) — orientation and file index.",
        "2. Files listed under **Memory Files** — current session state.",
        "3. Files listed under **Agent-Bootstrap Context Files** — repo governance context.",
        "",
        "To regenerate: `script/context-export`",
        "To verify memory files: `script/memory-drift-check`",
        "",
        f"Bundle hash (full): `{bundle_hash}`",
    ]

    return "\n".join(lines) + "\n"


def _build_pointers_json(entries: list[FileEntry], bundle_hash: str) -> str:
    """Build the pointers.json content (deterministic, JSON-sorted keys)."""
    data = {
        "bundle_hash": bundle_hash,
        "file_count": len(entries),
        "files": [
            {
                "path": e.path,
                "sha256": e.sha256,
                "size_bytes": e.size_bytes,
                "source": e.source,
            }
            for e in sorted(entries, key=lambda e: e.path)
        ],
        "schema": "context-bundle/1.0",
    }
    return json.dumps(data, indent=2, sort_keys=True) + "\n"


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------


def export_bundle(
    repo_root: Path,
    *,
    out_dir: Path | None = None,
    dry_run: bool = False,
) -> ExportResult:
    """Collect memory + context files and write ``context_bundle/``.

    Parameters
    ----------
    repo_root:
        Absolute path to the repository root.
    out_dir:
        Output directory for the bundle. Defaults to ``repo_root / "context_bundle"``.
    dry_run:
        If True, compute results but write nothing to disk.

    Returns
    -------
    ExportResult
        Contains file count, bundle hash, and output paths (None in dry-run mode).
    """
    out_dir = out_dir or (repo_root / "context_bundle")

    memory_dir = repo_root / "memory"
    context_current_dir = repo_root / "context" / "current"

    entries: list[FileEntry] = []
    entries.extend(_collect_memory_files(memory_dir, repo_root))
    entries.extend(_collect_context_files(context_current_dir, repo_root))

    # Sort globally by path for determinism
    entries.sort(key=lambda e: e.path)

    bh = _bundle_hash(entries)
    bundle_md_content = _build_bundle_md(entries, bh)
    pointers_json_content = _build_pointers_json(entries, bh)

    if dry_run:
        return ExportResult(
            file_count=len(entries),
            bundle_hash=bh,
            entries=entries,
            dry_run=True,
        )

    out_dir.mkdir(parents=True, exist_ok=True)
    bundle_md_path = out_dir / "BUNDLE.md"
    pointers_json_path = out_dir / "pointers.json"

    bundle_md_path.write_text(bundle_md_content, encoding="utf-8")
    pointers_json_path.write_text(pointers_json_content, encoding="utf-8")

    return ExportResult(
        file_count=len(entries),
        bundle_hash=bh,
        entries=entries,
        bundle_md_path=bundle_md_path,
        pointers_json_path=pointers_json_path,
        dry_run=False,
    )


# ---------------------------------------------------------------------------
# CLI entry point (called by script/context-export shim)
# ---------------------------------------------------------------------------


def main(argv: list[str] | None = None) -> int:
    import argparse

    parser = argparse.ArgumentParser(
        description="Generate a deterministic context bundle from memory/ and context/current/."
    )
    parser.add_argument(
        "--out",
        metavar="DIR",
        help="Output directory (default: context_bundle/)",
    )
    parser.add_argument(
        "--dry-run",
        action="store_true",
        help="Compute bundle without writing any files.",
    )
    parser.add_argument(
        "--repo-root",
        metavar="PATH",
        help="Repository root (default: parent of script/_lib/)",
    )
    args = parser.parse_args(argv)

    repo_root = Path(args.repo_root).resolve() if args.repo_root else Path(__file__).resolve().parent.parent.parent  # noqa: E501
    out_dir = Path(args.out).resolve() if args.out else None

    result = export_bundle(repo_root=repo_root, out_dir=out_dir, dry_run=args.dry_run)

    if args.dry_run:
        print(f"[dry-run] Would bundle {result.file_count} file(s).")
        print(f"[dry-run] Bundle hash: {result.bundle_hash}")
        for e in result.entries:
            print(f"  [{e.source}] {e.path}")
        print("[dry-run] No files written.")
    else:
        print(f"Exported {result.file_count} file(s) to {result.pointers_json_path.parent}")
        print(f"  BUNDLE.md:      {result.bundle_md_path}")
        print(f"  pointers.json:  {result.pointers_json_path}")
        print(f"  Bundle hash:    {result.bundle_hash}")

    return 0


if __name__ == "__main__":
    sys.exit(main())
