"""Longitudinal drift monitor for PRD Plugin agentic sessions.

Phase 1: observation only. The monitor snapshots existing validator outputs,
records state-mutating actions, computes a drift score, and stores append-only
JSONL history. It does not block, fail, or enforce anything.
"""

from __future__ import annotations

import argparse
import hashlib
import json
import os
import re
import subprocess
import sys
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any


SECRET_PATTERNS = ("TOKEN", "SECRET", "KEY", "PASSWORD", "PASS", "CREDENTIAL")
LOCAL_DRIFT_DIR = ".prd_plugin/local/drift"
SNAPSHOTS_FILE = "snapshots.jsonl"
ACTIONS_FILE = "actions.jsonl"
# REQ-067: a compact, versioned, append-only drift-event feed for external
# consumers (AI-Collab et al.) — one lean line per drift run. Distinct from the
# verbose snapshots.jsonl (which keeps full per-validator findings).
EVENTS_FILE = "events.jsonl"
EVENT_SCHEMA_VERSION = "1.0"
# REQ-069: when drift.monitoring.export.enabled is set, each event is ALSO appended
# to a committed (tracked, non-local) path so drift history travels with the repo
# and shared tools can read it across machines. Default off.
DEFAULT_EXPORT_PATH = ".prd_plugin/drift/events.jsonl"

DEFAULT_VALIDATORS = [
    "state_consistency_check",
    "prd_doctor",
    "release_check",
    "gap_audit",
    "request_report",
    # Added REQ-065: the detection surfaces the monitor was previously blind to —
    # the composed gate (+ its newer checks), docs-vs-reality drift, traceability
    # gaps, and stale tracking items.
    "prd_gate",
    "prd_self_audit",
    "prd_graph",
    "staleness_audit",
    "wiki_drift",
    "ingest_manual_drift",
    "fork_drift",
]

DEFAULT_WEIGHTS = {
    "state_consistency_check": 1.0,
    "prd_doctor": 1.0,
    "release_check": 1.0,
    "gap_audit": 1.0,
    "request_report": 1.0,
    "prd_gate": 1.0,
    "prd_self_audit": 1.0,
    "prd_graph": 1.0,
    "staleness_audit": 1.0,
    "wiki_drift": 1.0,
    "ingest_manual_drift": 1.0,
    "fork_drift": 1.0,
}

# The cheap subset run on every Stop: tracking + docs + traceability drift, no
# heavy version/gap/release scans.
ON_STOP_VALIDATORS = [
    "state_consistency_check",
    "staleness_audit",
    "prd_self_audit",
    "prd_graph",
    "wiki_drift",
    "ingest_manual_drift",
    "fork_drift",
]

ARCHETYPE_PATTERNS = {
    "missing_claimed_id": re.compile(r"\b(CONS-ERR-001)\b"),
    "registry_counter_gap": re.compile(r"\b(CONS-ERR-002)\b"),
    "future_timestamp": re.compile(r"\b(CONS-ERR-003)\b"),
    "state_consistency_error": re.compile(r"\b(CONS-ERR-)"),
    "doctor_error": re.compile(r"\b(DOC-ERR-)"),
    "doctor_warning": re.compile(r"\b(DOC-WARN-)"),
    "release_hygiene_high": re.compile(r'"severity":\s*"high"'),
    "release_hygiene_medium": re.compile(r'"severity":\s*"medium"'),
    "request_needs_attention": re.compile(r"needs attention"),
    "privacy_warning": re.compile(r"privacy warning"),
    "stale_request": re.compile(r"stale request"),
    "hub_only_script_downstream": re.compile(r"hub/development scripts"),
    "plugin_dev_skill_downstream": re.compile(r"development workflows"),
    "missing_repo_local_skill": re.compile(r"skills are missing"),
    # REQ-065: newer finding types the categorizer was missing.
    "unsubmitted_plugin_request": re.compile(r"unsubmitted_plugin_request"),
    "duplicate_id": re.compile(r"duplicate_id|Duplicate id"),
    "implemented_without_graduated": re.compile(r"graduated_to|implemented_requires_graduated"),
    "stranded_outbox": re.compile(r"stranded_outbox"),
    "version_marker_drift": re.compile(r"version[_-]marker|marker.*drift"),
    "timescale_estimate": re.compile(r"timescale|time estimate"),
    "traceability_gap": re.compile(r"\borphan|incomplete chain|dangling|requirement_without_coverage"),
    "stale_item": re.compile(r"\bstale\b"),
    "docs_reality_gap": re.compile(r"rule-vs-reality|rule gap|stated rule|not enforced"),
    "wiki_drift": re.compile(r"wiki_drift|wiki article .*behind HEAD|no Commit stamp"),
    "ingest_manual_drift": re.compile(r"ingest_manual_drift|ingest manual .*(behind HEAD|cited source changed)|no longer conformant"),
    "fork_drift": re.compile(r"fork_drift|new fork version is available"),
}


def _now_iso() -> str:
    return datetime.now(timezone.utc).isoformat()


def _read_json(path: Path) -> Any:
    return json.loads(path.read_text(encoding="utf-8-sig"))


def _drift_dir(repo_root: Path) -> Path:
    return (repo_root / LOCAL_DRIFT_DIR).resolve()


def _ensure_drift_dir(repo_root: Path) -> Path:
    d = _drift_dir(repo_root)
    d.mkdir(parents=True, exist_ok=True)
    return d


def _append_jsonl(path: Path, record: dict[str, Any]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("a", encoding="utf-8") as f:
        f.write(json.dumps(record, ensure_ascii=False) + "\n")


def _load_jsonl(path: Path) -> list[dict[str, Any]]:
    if not path.exists():
        return []
    records = []
    for line in path.read_text(encoding="utf-8-sig").splitlines():
        line = line.strip()
        if not line:
            continue
        try:
            records.append(json.loads(line))
        except json.JSONDecodeError:
            continue
    return records


def _redact_value(value: Any) -> Any:
    if value is None:
        return None
    text = str(value)
    if any(pattern in text.upper() for pattern in SECRET_PATTERNS):
        return "***"
    if re.search(r"[A-Za-z]:[\\/]", text) or text.startswith(("/Users/", "/home/")):
        return "<local-path>"
    return value


def _redact_command(command: str) -> str:
    parts = command.split()
    redacted = []
    for part in parts:
        redacted.append(_redact_value(part))
    return " ".join(str(p) for p in redacted)


def _redact_paths(paths: list[str]) -> list[str]:
    return [str(_redact_value(p)) for p in paths]


def _state_affected(path: str) -> bool:
    p = path.replace("\\", "/")
    return (
        p.startswith(".prd_plugin/")
        or p.startswith("docs/evidence/")
        or p.startswith("docs/traceability/")
        or p.startswith("docs/decisions/")
        or p.startswith("docs/prd/")
        or p.startswith("docs/architecture/")
        or p.startswith("docs/implementation/")
    )


def load_config(repo_root: Path) -> dict[str, Any]:
    path = repo_root / ".prd_plugin" / "config.json"
    if not path.exists():
        return {}
    try:
        data = _read_json(path)
        return data if isinstance(data, dict) else {}
    except (OSError, json.JSONDecodeError):
        return {}


def drift_config(config: dict[str, Any]) -> dict[str, Any]:
    drift = config.get("drift", {})
    monitoring = drift.get("monitoring", {}) if isinstance(drift, dict) else {}
    return {
        "enabled": bool(monitoring.get("enabled", True)),
        "weights": {**DEFAULT_WEIGHTS, **(monitoring.get("weights") or {})},
        "validators": list(monitoring.get("validators") or DEFAULT_VALIDATORS),
        "archetypes": monitoring.get("archetypes", {}),
        # REQ-065: run a cheap drift check on every Stop when enabled (default off).
        "on_stop": bool(monitoring.get("on_stop", False)),
        "on_stop_validators": list(monitoring.get("on_stop_validators") or ON_STOP_VALIDATORS),
    }


SCRIPT_MAP = {
    "state_consistency_check": "scripts/state_consistency_check.py",
    "prd_doctor": "scripts/prd_doctor.py",
    "release_check": "scripts/release_check.py",
    "gap_audit": "scripts/gap_audit.py",
    "request_report": "scripts/request_report.py",
    # REQ-065 additions:
    "prd_gate": "scripts/prd_gate.py",
    "prd_self_audit": "scripts/prd_self_audit.py",
    "prd_graph": "scripts/prd_graph.py",
    "staleness_audit": "scripts/staleness_audit.py",
    "wiki_drift": "scripts/prd_wiki_backfill.py",
    "ingest_manual_drift": "scripts/ingest_manual_drift.py",
    "fork_drift": "scripts/fork_version_check.py",
}


def validator_command(repo_root: Path, validator: str) -> list[str]:
    """The subprocess command for a validator. Most speak the standard
    --repo-root/--format json contract; a few need their own invocation."""
    script = SCRIPT_MAP.get(validator)
    if not script:
        return []
    version = load_plugin_version(repo_root)
    output_file = repo_root / ".prd_plugin" / "local" / "drift" / f"validator-{validator}.json"

    if validator == "gap_audit":
        return [sys.executable, script, "--target-version", version or "0.0.0",
                "--format", "json", "--output", str(output_file)]
    if validator in ("request_report", "message_check"):
        return [sys.executable, script, "--config",
                str(repo_root / ".prd_plugin" / "config.json"),
                "--format", "json", "--output", str(output_file)]
    if validator == "prd_gate":
        # composed gate; the 'check' subcommand emits the findings surface
        return [sys.executable, script, "check", "--repo-root", str(repo_root),
                "--format", "json"]
    if validator in ("wiki_drift", "ingest_manual_drift", "fork_drift"):
        return [sys.executable, script, "--repo-root", str(repo_root), "--drift", "--format", "json"]
    if validator == "prd_graph":
        # traceability gaps; note --out (not --output) and the --gaps mode
        return [sys.executable, script, "--repo-root", str(repo_root),
                "--gaps", "--format", "json"]
    # default contract (state_consistency_check, prd_doctor, release_check,
    # prd_self_audit, staleness_audit)
    return [sys.executable, script, "--repo-root", str(repo_root),
            "--format", "json", "--output", str(output_file)]


def _run_validator(repo_root: Path, validator: str) -> dict[str, Any]:
    cmd = validator_command(repo_root, validator)
    if not cmd:
        return {
            "validator": validator,
            "status": "unknown",
            "error": f"No script mapping for validator {validator}",
            "findings": [],
        }

    try:
        result = subprocess.run(
            cmd,
            cwd=str(repo_root),
            capture_output=True,
            text=True,
            timeout=60,
        )
    except subprocess.TimeoutExpired:
        return {
            "validator": validator,
            "status": "timeout",
            "error": "Validator timed out after 60 seconds",
            "findings": [],
        }
    except Exception as exc:
        return {
            "validator": validator,
            "status": "error",
            "error": str(exc),
            "findings": [],
        }

    # Prefer the validator's --output file; fall back to stdout for validators
    # that only print (prd_gate, prd_graph).
    output_file = repo_root / ".prd_plugin" / "local" / "drift" / f"validator-{validator}.json"
    data = None
    try:
        data = json.loads(output_file.read_text(encoding="utf-8-sig"))
    except (OSError, json.JSONDecodeError):
        try:
            data = json.loads(result.stdout)
        except (json.JSONDecodeError, ValueError):
            data = {
                "status": "error",
                "stdout_preview": result.stdout[:500],
                "stderr_preview": result.stderr[:500],
            }

    findings = data.get("findings", [])
    if not isinstance(findings, list):
        findings = []

    return {
        "validator": validator,
        "status": data.get("status", "ok"),
        "findings": findings,
        "summary": data.get("summary", {}),
    }


def load_plugin_version(repo_root: Path) -> str | None:
    for rel in (
        ".codex-plugin/plugin.json",
        ".opencode/plugin.json",
        ".claude-plugin/plugin.json",
    ):
        path = repo_root / rel
        if path.is_file():
            try:
                data = _read_json(path)
                version = data.get("version")
                if version:
                    return str(version)
            except (OSError, json.JSONDecodeError):
                continue
    # Downstream repos carry the version in .prd_plugin/config.json, not a host
    # manifest — fall back to it so drift events are version-stamped everywhere.
    cfg_path = repo_root / ".prd_plugin" / "config.json"
    if cfg_path.is_file():
        try:
            plugin = _read_json(cfg_path).get("plugin", {})
            version = plugin.get("installed_version") or plugin.get("version")
            if version:
                return str(version)
        except (OSError, json.JSONDecodeError, AttributeError):
            pass
    return None


def build_snapshot(repo_root: Path, config: dict[str, Any] | None = None) -> dict[str, Any]:
    cfg = drift_config(config or load_config(repo_root))
    snapshot = {
        "schema_version": "0.1",
        "type": "snapshot",
        "snapshot_id": f"SNAPSHOT-{uuid.uuid4().hex[:8].upper()}",
        "timestamp": _now_iso(),
        "validators": {},
    }
    for validator in cfg["validators"]:
        snapshot["validators"][validator] = _run_validator(repo_root, validator)
    return snapshot


def _finding_weight(finding: dict[str, Any]) -> float:
    severity = str(finding.get("severity", "info")).lower()
    return {"error": 3.0, "critical": 5.0, "high": 2.0, "warning": 1.0, "info": 0.0}.get(severity, 0.0)


def compute_drift_score(snapshot: dict[str, Any], weights: dict[str, float] | None = None) -> tuple[float, dict[str, float]]:
    weights = weights or DEFAULT_WEIGHTS
    per_validator: dict[str, float] = {}
    total = 0.0
    for validator, result in snapshot.get("validators", {}).items():
        w = float(weights.get(validator, 1.0))
        score = sum(_finding_weight(f) for f in result.get("findings", []))
        weighted = score * w
        per_validator[validator] = weighted
        total += weighted
    return round(total, 2), per_validator


def classify_archetypes(snapshot: dict[str, Any]) -> list[str]:
    text = json.dumps(snapshot.get("validators", {}), ensure_ascii=False)
    archetypes = []
    for name, pattern in ARCHETYPE_PATTERNS.items():
        if pattern.search(text):
            archetypes.append(name)
    return sorted(set(archetypes))


def record_action(
    repo_root: Path,
    session_id: str,
    action_type: str,
    params: dict[str, Any],
    timestamp: str | None = None,
) -> dict[str, Any]:
    action = {
        "schema_version": "0.1",
        "type": "action",
        "action_id": f"ACTION-{uuid.uuid4().hex[:8].upper()}",
        "session_id": session_id,
        "timestamp": timestamp or _now_iso(),
        "action_type": action_type,
    }
    if action_type == "write":
        action["file"] = _redact_value(params.get("file", ""))
        action["state_affected"] = _state_affected(str(params.get("file", "")))
    elif action_type in ("shell", "bash"):
        raw_cmd = params.get("command", "")
        action["command"] = _redact_command(raw_cmd)
        action["script_match"] = re.findall(r"\b([\w_]+\.py)\b", raw_cmd)
    elif action_type == "read":
        action["file"] = _redact_value(params.get("file", ""))
    else:
        action["params_keys"] = sorted(params.keys())

    _append_jsonl(_drift_dir(repo_root) / ACTIONS_FILE, action)
    return action


def session_start(repo_root: Path, session_id: str | None = None) -> dict[str, Any]:
    sid = session_id or f"SES-{uuid.uuid4().hex[:8].upper()}"
    snapshot = build_snapshot(repo_root)
    snapshot["type"] = "session_start"
    snapshot["session_id"] = sid
    score, per_validator = compute_drift_score(snapshot, drift_config(load_config(repo_root))["weights"])
    snapshot["drift_score"] = score
    snapshot["drift_per_validator"] = per_validator
    snapshot["archetypes"] = classify_archetypes(snapshot)
    _append_jsonl(_drift_dir(repo_root) / SNAPSHOTS_FILE, snapshot)
    _append_drift_event(repo_root, snapshot)
    return snapshot


def session_end(repo_root: Path, session_id: str) -> dict[str, Any]:
    snapshot = build_snapshot(repo_root)
    snapshot["type"] = "session_end"
    snapshot["session_id"] = session_id
    score, per_validator = compute_drift_score(snapshot, drift_config(load_config(repo_root))["weights"])
    snapshot["drift_score"] = score
    snapshot["drift_per_validator"] = per_validator
    snapshot["archetypes"] = classify_archetypes(snapshot)
    _append_jsonl(_drift_dir(repo_root) / SNAPSHOTS_FILE, snapshot)
    _append_drift_event(repo_root, snapshot)
    return snapshot


def drift_event_from_snapshot(snapshot: dict[str, Any], plugin_version: str | None = None) -> dict[str, Any]:
    """Derive a compact, versioned drift event from a snapshot — the stable feed
    external tools consume. Carries counts and archetypes, never inline findings.

    Schema (schema_version 1.0): event_id, timestamp, type, plugin_version,
    drift_score, total_findings, by_validator{name:count}, archetypes[].
    """
    summary = summarize_snapshot(snapshot)
    return {
        "schema_version": EVENT_SCHEMA_VERSION,
        "event_id": f"DRIFT-EVT-{uuid.uuid4().hex[:8].upper()}",
        "timestamp": snapshot.get("timestamp") or _now_iso(),
        "type": snapshot.get("type", "manual"),
        "plugin_version": plugin_version,
        "drift_score": snapshot.get("drift_score"),
        "total_findings": summary["total"],
        "by_validator": summary["by_validator"],
        "archetypes": snapshot.get("archetypes", []),
    }


def _export_settings(repo_root: Path) -> dict[str, Any]:
    """Read drift.monitoring.export {enabled, path}. Default off, default path is
    the committed (non-local) DEFAULT_EXPORT_PATH."""
    mon = load_config(repo_root).get("drift", {}).get("monitoring", {})
    export = mon.get("export", {}) if isinstance(mon, dict) else {}
    if not isinstance(export, dict):
        export = {}
    return {"enabled": bool(export.get("enabled", False)),
            "path": export.get("path") or DEFAULT_EXPORT_PATH}


def _append_drift_event(repo_root: Path, snapshot: dict[str, Any]) -> dict[str, Any]:
    """Append one compact event to events.jsonl. Best-effort — a telemetry write
    must never break the drift run that produced the snapshot. When export is
    enabled, also append to the committed/shared feed (REQ-069)."""
    try:
        event = drift_event_from_snapshot(snapshot, load_plugin_version(repo_root))
        _append_jsonl(_drift_dir(repo_root) / EVENTS_FILE, event)
        export = _export_settings(repo_root)
        if export["enabled"]:
            try:
                _append_jsonl(Path(repo_root) / export["path"], event)
            except Exception:  # pragma: no cover - export is best-effort
                pass
        return event
    except Exception:  # pragma: no cover - telemetry is best-effort
        return {}


def read_drift_events(repo_root: Path, limit: int | None = None) -> list[dict[str, Any]]:
    """Read the drift-event feed (oldest first). `limit` returns the most recent
    N. This is the supported programmatic entry point for consumers."""
    events = _load_jsonl(_drift_dir(repo_root) / EVENTS_FILE)
    if limit is not None and limit >= 0:
        return events[-limit:]
    return events


def summarize_snapshot(snapshot: dict[str, Any]) -> dict[str, Any]:
    """Total findings and a one-line human summary for a stop-check snapshot."""
    per_validator = {}
    total = 0
    for validator, result in snapshot.get("validators", {}).items():
        findings = result.get("findings") or []
        n = len(findings) if isinstance(findings, list) else 0
        # a validator that reports a non-ok status with no structured findings
        # still counts as one signal
        if n == 0 and str(result.get("status", "")).lower() in ("attention", "error", "fail"):
            n = 1
        if n:
            per_validator[validator] = n
            total += n
    if total == 0:
        line = "no tracking/docs drift detected"
    else:
        parts = ", ".join(f"{n} from {v}" for v, n in sorted(per_validator.items(), key=lambda kv: -kv[1]))
        line = f"{total} drift finding(s): {parts}. Run `/prd-drift run` for detail."
    return {"total": total, "by_validator": per_validator, "summary": line}


def stop_check(repo_root: Path) -> dict[str, Any]:
    """Run the cheap on-stop validator set and return a summary. Observation-only,
    never raises. Skipped unless drift.monitoring.enabled and on_stop are set."""
    try:
        cfg = drift_config(load_config(repo_root))
        if not cfg["enabled"]:
            return {"ran": False, "reason": "drift monitoring disabled"}
        if not cfg["on_stop"]:
            return {"ran": False, "reason": "on_stop not enabled"}
        # run only the cheap on-stop subset
        override = {"drift": {"monitoring": {
            "enabled": True,
            "validators": cfg["on_stop_validators"],
            "weights": cfg["weights"],
        }}}
        snapshot = build_snapshot(repo_root, override)
        snapshot["type"] = "stop_check"
        score, per_validator = compute_drift_score(snapshot, cfg["weights"])
        snapshot["drift_score"] = score
        snapshot["archetypes"] = classify_archetypes(snapshot)
        _append_jsonl(_drift_dir(repo_root) / SNAPSHOTS_FILE, snapshot)
        _append_drift_event(repo_root, snapshot)
        summary = summarize_snapshot(snapshot)
        summary["ran"] = True
        summary["drift_score"] = score
        return summary
    except Exception as exc:  # pragma: no cover - fail-open backstop
        return {"ran": False, "reason": f"error: {type(exc).__name__}: {exc}"}


def record_delta(repo_root: Path, session_id: str) -> dict[str, Any]:
    snapshot = build_snapshot(repo_root)
    snapshot["type"] = "delta"
    snapshot["session_id"] = session_id
    score, per_validator = compute_drift_score(snapshot, drift_config(load_config(repo_root))["weights"])
    snapshot["drift_score"] = score
    snapshot["drift_per_validator"] = per_validator
    snapshot["archetypes"] = classify_archetypes(snapshot)
    _append_jsonl(_drift_dir(repo_root) / SNAPSHOTS_FILE, snapshot)
    _append_drift_event(repo_root, snapshot)
    return snapshot


def build_report(repo_root: Path, session_id: str | None = None) -> dict[str, Any]:
    snapshots = _load_jsonl(_drift_dir(repo_root) / SNAPSHOTS_FILE)
    actions = _load_jsonl(_drift_dir(repo_root) / ACTIONS_FILE)

    if session_id:
        session_snapshots = [s for s in snapshots if s.get("session_id") == session_id]
        session_actions = [a for a in actions if a.get("session_id") == session_id]
    else:
        session_snapshots = snapshots
        session_actions = actions

    if not session_snapshots:
        return {
            "status": "ok",
            "repo_root": str(repo_root),
            "session_id": session_id,
            "summary": {"baseline": 0, "final": 0, "delta": 0},
            "findings": {"new": [], "resolved": []},
            "archetypes": [],
            "actions_count": len(session_actions),
        }

    baseline = session_snapshots[0]
    final = session_snapshots[-1]
    baseline_score = baseline.get("drift_score", 0)
    final_score = final.get("drift_score", 0)

    baseline_findings = {
        f"{v}|{f.get('id')}|{f.get('summary', '')[:80]}"
        for s in [baseline]
        for v, r in s.get("validators", {}).items()
        for f in r.get("findings", [])
    }
    final_findings = {
        f"{v}|{f.get('id')}|{f.get('summary', '')[:80]}"
        for s in [final]
        for v, r in s.get("validators", {}).items()
        for f in r.get("findings", [])
    }

    def _detail(key: str) -> dict[str, Any]:
        parts = key.split("|", 2)
        return {"validator": parts[0], "id": parts[1], "summary": parts[2]}

    return {
        "status": "ok",
        "repo_root": str(repo_root),
        "session_id": session_id,
        "summary": {
            "baseline": baseline_score,
            "final": final_score,
            "delta": round(final_score - baseline_score, 2),
        },
        "findings": {
            "new": [_detail(k) for k in sorted(final_findings - baseline_findings)],
            "resolved": [_detail(k) for k in sorted(baseline_findings - final_findings)],
        },
        "archetypes": sorted(set(final.get("archetypes", [])) | set(baseline.get("archetypes", []))),
        "actions_count": len(session_actions),
        "snapshots_count": len(session_snapshots),
    }


def format_report_markdown(report: dict[str, Any]) -> str:
    lines = [
        "# PRD Plugin Drift Report",
        "",
        f"Repo: `{report['repo_root']}`",
        f"Session: `{report.get('session_id') or 'all'}`",
        "",
        "| Metric | Value |",
        "| --- | --- |",
        f"| Baseline drift | {report['summary']['baseline']} |",
        f"| Final drift | {report['summary']['final']} |",
        f"| Delta | {report['summary']['delta']} |",
        f"| Actions recorded | {report['actions_count']} |",
        f"| Snapshots | {report.get('snapshots_count', 0)} |",
        "",
        "## Archetypes",
        "",
    ]
    if report["archetypes"]:
        for archetype in report["archetypes"]:
            lines.append(f"- {archetype}")
    else:
        lines.append("- None")
    lines.extend(["", "## New findings", ""])
    if report["findings"]["new"]:
        for finding in report["findings"]["new"]:
            lines.append(f"- `{finding['validator']}` `{finding['id']}`: {finding['summary']}")
    else:
        lines.append("- None")
    lines.extend(["", "## Resolved findings", ""])
    if report["findings"]["resolved"]:
        for finding in report["findings"]["resolved"]:
            lines.append(f"- `{finding['validator']}` `{finding['id']}`: {finding['summary']}")
    else:
        lines.append("- None")
    return "\n".join(lines) + "\n"


def main(argv=None):
    parser = argparse.ArgumentParser(description="Longitudinal drift monitor for PRD Plugin sessions (observation-only).")
    parser.add_argument("--repo-root", default=".", help="Repository root to monitor.")
    parser.add_argument("--session-id", help="Session identifier. Generated if omitted.")
    parser.add_argument("--snapshot", action="store_true", help="Record a single snapshot.")
    parser.add_argument("--session-start", action="store_true", help="Record a session-start snapshot.")
    parser.add_argument("--session-end", action="store_true", help="Record a session-end snapshot.")
    parser.add_argument("--delta", action="store_true", help="Record a delta snapshot.")
    parser.add_argument("--report", action="store_true", help="Emit a drift report for the session.")
    parser.add_argument("--record-action", help="Record an action: write,shell,bash,read")
    parser.add_argument("--action-params", default="{}", help="JSON params for --record-action.")
    parser.add_argument("--format", choices=("json", "markdown"), default="json")
    parser.add_argument("--output", help="Optional output path.")
    parser.add_argument("--log", action="store_true",
                        help="Read the compact drift-event feed (events.jsonl) for consumers.")
    parser.add_argument("--limit", type=int, default=None,
                        help="With --log: return only the most recent N events.")
    args = parser.parse_args(argv)

    repo_root = Path(args.repo_root).resolve()

    # --log reads the event feed and is independent of whether monitoring is
    # currently enabled — a consumer may read history at any time.
    if args.log:
        events = read_drift_events(repo_root, args.limit)
        if args.format == "markdown":
            lines = ["# Drift events", ""]
            for e in events:
                lines.append(f"- {e.get('timestamp')} `{e.get('type')}` "
                             f"score={e.get('drift_score')} findings={e.get('total_findings')} "
                             f"{e.get('by_validator')}")
            content = "\n".join(lines) + "\n"
        else:
            content = json.dumps(events, indent=2) + "\n"
        if args.output:
            Path(args.output).write_text(content, encoding="utf-8")
        else:
            print(content, end="")
        return 0

    config = load_config(repo_root)
    cfg = drift_config(config)
    if not cfg["enabled"]:
        result = {"status": "disabled", "reason": "drift.monitoring.enabled is false"}
        content = json.dumps(result, indent=2) + "\n"
        if args.output:
            Path(args.output).write_text(content, encoding="utf-8")
        else:
            print(content, end="")
        return 0

    if args.session_start:
        result = session_start(repo_root, args.session_id)
    elif args.session_end:
        result = session_end(repo_root, args.session_id or "UNKNOWN")
    elif args.delta:
        result = record_delta(repo_root, args.session_id or "UNKNOWN")
    elif args.snapshot:
        result = build_snapshot(repo_root, config)
        result["type"] = "snapshot"
        score, per_validator = compute_drift_score(result, cfg["weights"])
        result["drift_score"] = score
        result["drift_per_validator"] = per_validator
        result["archetypes"] = classify_archetypes(result)
    elif args.report:
        result = build_report(repo_root, args.session_id)
    elif args.record_action:
        params = json.loads(args.action_params)
        result = record_action(repo_root, args.session_id or "UNKNOWN", args.record_action, params)
    else:
        parser.error("Specify one of --snapshot, --session-start, --session-end, --delta, --report, --record-action")

    if args.format == "json":
        content = json.dumps(result, indent=2) + "\n"
    elif args.report:
        content = format_report_markdown(result)
    else:
        content = json.dumps(result, indent=2) + "\n"

    if args.output:
        output = Path(args.output)
        output.parent.mkdir(parents=True, exist_ok=True)
        output.write_text(content, encoding="utf-8")
    else:
        print(content, end="")

    return 0


if __name__ == "__main__":
    raise SystemExit(main())
