#!/usr/bin/env python3
"""P9 inbox migration: from-originator/from-agent/followup → unified inbox/ + sibling followup/.

Hard cutover. Run with --dry-run first to preview all proposed moves.

Usage:
    python bin/migrate-inbox-scheme --dry-run   # preview; no changes
    python bin/migrate-inbox-scheme             # execute

Migration rules per track:
  inbox/from-originator/*.md       → inbox/{file}.md       (frontmatter: from=originator, to={track})
  inbox/from-originator/.consumed/ → inbox/.consumed/      (with frontmatter)
  inbox/from-agent/*.md            → tracks/originator/inbox/{file}.md  (from={track}, to=originator)
  inbox/followup/*.md              → followup/{file}.md    (sibling, no frontmatter change)
  inbox/followup/.completed/       → followup/.completed/

AGENT-INTERNAL: some from-agent messages may be destined for other agents
(e.g., crimes-worker deploy requests). These are flagged in the output with
[TRIAGE] markers. Review flagged files before or after migration.
"""
from __future__ import annotations

import argparse
import os
import re
import shutil
import sys
from datetime import datetime, timezone
from pathlib import Path

PROJECT_ROOT = Path(__file__).resolve().parent.parent
TRACKS_DIR = PROJECT_ROOT / "agent" / "runtime" / "tracks"

AGENT_TO_AGENT_KEYWORDS = [
    "deploy", "deploy-request", "crimes-worker", "dispatch", "crimes_worker",
    "from-agent/deployed", ".deployed",
]


def _mtime_iso(path: Path) -> str:
    """Return mtime as ISO 8601 string."""
    try:
        mt = path.stat().st_mtime
        return datetime.fromtimestamp(mt, tz=timezone.utc).strftime("%Y-%m-%dT%H-%M-%SZ")
    except OSError:
        return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H-%M-%SZ")


def _inject_frontmatter(content: str, from_: str, to: str, at: str, kind: str = "report") -> str:
    """Prepend YAML frontmatter unless content already starts with ---."""
    if content.startswith("---"):
        return content  # already has frontmatter — leave it alone
    fm = f"---\nfrom: {from_}\nto: {to}\nat: {at}\nkind: {kind}\n---\n\n"
    return fm + content


def _is_agent_to_agent(path: Path, content: str) -> bool:
    """Heuristic: is this from-agent message actually destined for another agent?"""
    combined = path.name.lower() + " " + content[:500].lower()
    return any(kw in combined for kw in AGENT_TO_AGENT_KEYWORDS)


def migrate_track(
    track_dir: Path,
    originator_inbox: Path,
    dry_run: bool,
    moves: list[dict],
) -> None:
    """Migrate one track's inbox to the P9 layout."""
    track_name = track_dir.name
    inbox_dir = track_dir / "inbox"
    from_orig = inbox_dir / "from-originator"
    from_agent = inbox_dir / "from-agent"
    followup_src = inbox_dir / "followup"
    followup_dst = track_dir / "followup"   # sibling

    # --- from-originator → inbox/ ---
    if from_orig.is_dir():
        target_dir = inbox_dir
        if not dry_run:
            target_dir.mkdir(parents=True, exist_ok=True)
        for p in sorted(from_orig.iterdir()):
            if p.name == ".consumed" or p.name.startswith("."):
                continue
            if p.suffix != ".md" or not p.is_file():
                continue
            dst = target_dir / p.name
            at = _mtime_iso(p)
            content = _inject_frontmatter(p.read_text(), from_="originator", to=track_name, at=at)
            moves.append({"op": "move+fm", "src": str(p.relative_to(PROJECT_ROOT)), "dst": str(dst.relative_to(PROJECT_ROOT)), "from_": "originator", "to": track_name})
            if not dry_run:
                dst.write_text(content)
                p.unlink()

        # from-originator/.consumed → inbox/.consumed/
        consumed_src = from_orig / ".consumed"
        consumed_dst = inbox_dir / ".consumed"
        if consumed_src.is_dir():
            if not dry_run:
                consumed_dst.mkdir(parents=True, exist_ok=True)
            for p in sorted(consumed_src.iterdir()):
                if p.suffix != ".md" or not p.is_file():
                    continue
                dst = consumed_dst / p.name
                at = _mtime_iso(p)
                content = _inject_frontmatter(p.read_text(), from_="originator", to=track_name, at=at)
                moves.append({"op": "move+fm(.consumed)", "src": str(p.relative_to(PROJECT_ROOT)), "dst": str(dst.relative_to(PROJECT_ROOT))})
                if not dry_run:
                    dst.write_text(content)
                    p.unlink()
            if not dry_run:
                try:
                    consumed_src.rmdir()
                except OSError:
                    pass  # non-empty — leave it

        if not dry_run:
            try:
                from_orig.rmdir()
            except OSError:
                pass  # non-empty — leave it

    # --- from-agent → originator track inbox (default) ---
    if from_agent.is_dir():
        for p in sorted(from_agent.iterdir()):
            if p.name in (".deployed", ".consumed") or p.name.startswith("."):
                subdir = p
                if subdir.is_dir():
                    # Archive subdirs: move contents to originator/.consumed
                    consumed_dst = originator_inbox / ".consumed"
                    if not dry_run:
                        consumed_dst.mkdir(parents=True, exist_ok=True)
                    for sp in sorted(subdir.iterdir()):
                        if sp.suffix != ".md" or not sp.is_file():
                            continue
                        dst = consumed_dst / sp.name
                        at = _mtime_iso(sp)
                        content = _inject_frontmatter(sp.read_text(), from_=track_name, to="originator", at=at)
                        moves.append({"op": "archive(.deployed→originator/.consumed)", "src": str(sp.relative_to(PROJECT_ROOT)), "dst": str(dst.relative_to(PROJECT_ROOT))})
                        if not dry_run:
                            dst.write_text(content)
                            sp.unlink()
                    if not dry_run:
                        try:
                            subdir.rmdir()
                        except OSError:
                            pass
                continue
            if p.suffix != ".md" or not p.is_file():
                continue
            # Heuristic triage: agent-to-agent?
            content_raw = p.read_text()
            triage = _is_agent_to_agent(p, content_raw)
            dst = originator_inbox / p.name
            at = _mtime_iso(p)
            content = _inject_frontmatter(content_raw, from_=track_name, to="originator", at=at)
            tag = "[TRIAGE:agent-to-agent?]" if triage else ""
            moves.append({"op": f"move+fm(from-agent→originator){tag}", "src": str(p.relative_to(PROJECT_ROOT)), "dst": str(dst.relative_to(PROJECT_ROOT)), "from_": track_name, "to": "originator", "triage": triage})
            if not dry_run:
                originator_inbox.mkdir(parents=True, exist_ok=True)
                dst.write_text(content)
                p.unlink()

        if not dry_run:
            try:
                from_agent.rmdir()
            except OSError:
                pass

    # --- followup → sibling followup/ ---
    if followup_src.is_dir():
        if not dry_run:
            followup_dst.mkdir(parents=True, exist_ok=True)
        completed_src = followup_src / ".completed"
        completed_dst = followup_dst / ".completed"
        for p in sorted(followup_src.iterdir()):
            if p.name == ".completed" or p.name.startswith("."):
                continue
            if p.suffix != ".md" or not p.is_file():
                continue
            dst = followup_dst / p.name
            moves.append({"op": "move(followup→sibling)", "src": str(p.relative_to(PROJECT_ROOT)), "dst": str(dst.relative_to(PROJECT_ROOT))})
            if not dry_run:
                shutil.move(str(p), str(dst))
        if completed_src.is_dir():
            if not dry_run:
                completed_dst.mkdir(parents=True, exist_ok=True)
            for p in sorted(completed_src.iterdir()):
                if p.suffix != ".md" or not p.is_file():
                    continue
                dst = completed_dst / p.name
                moves.append({"op": "move(followup/.completed→sibling)", "src": str(p.relative_to(PROJECT_ROOT)), "dst": str(dst.relative_to(PROJECT_ROOT))})
                if not dry_run:
                    shutil.move(str(p), str(dst))
            if not dry_run:
                try:
                    completed_src.rmdir()
                except OSError:
                    pass
        if not dry_run:
            try:
                followup_src.rmdir()
            except OSError:
                pass


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument("--dry-run", action="store_true", help="Preview moves without executing")
    args = parser.parse_args()

    dry_run = args.dry_run
    mode = "DRY RUN" if dry_run else "EXECUTE"
    print(f"migrate-inbox-scheme [{mode}]")
    print(f"Project root: {PROJECT_ROOT}")
    print()

    # Ensure originator track exists
    originator_dir = TRACKS_DIR / "originator"
    originator_inbox = originator_dir / "inbox"
    if not dry_run:
        (originator_inbox / ".consumed").mkdir(parents=True, exist_ok=True)
        human_flag = originator_dir / "human.flag"
        if not human_flag.exists():
            human_flag.touch()

    all_moves: list[dict] = []
    triage_count = 0

    for track_dir in sorted(TRACKS_DIR.iterdir()):
        if not track_dir.is_dir():
            continue
        if track_dir.name.startswith("."):
            continue
        if (track_dir / "human.flag").exists():
            print(f"  [{track_dir.name}] skipped (human track)")
            continue

        track_moves: list[dict] = []
        migrate_track(track_dir, originator_inbox, dry_run, track_moves)
        if track_moves:
            print(f"  [{track_dir.name}] {len(track_moves)} move(s):")
            for m in track_moves:
                triage = " ⚠ TRIAGE" if m.get("triage") else ""
                print(f"    {m['op']}{triage}")
                print(f"      {m['src']}")
                print(f"      → {m['dst']}")
            triage_count += sum(1 for m in track_moves if m.get("triage"))
            all_moves.extend(track_moves)
        else:
            print(f"  [{track_dir.name}] nothing to migrate")

    print()
    print(f"Total: {len(all_moves)} move(s)")
    if triage_count:
        print(f"⚠  {triage_count} message(s) flagged [TRIAGE] — may be agent-to-agent mail, not originator mail.")
        print("   Review flagged destinations in tracks/originator/inbox/ and re-route if needed.")

    if dry_run:
        print()
        print("Dry run complete. Re-run without --dry-run to execute.")
    else:
        print("Migration complete.")


if __name__ == "__main__":
    main()
