#!/usr/bin/env python3
"""memory-lint — READ-ONLY memory health report (B4, zero-write).

Consolidates EXISTING SELECT-only audit + health detectors into a single
printed lint report. This CLI is *incapable of writing*: it opens the memory
database in SQLite read-only URI mode (``?mode=ro``) and additionally pins the
connection with ``PRAGMA query_only=ON`` (belt + suspenders). It NEVER calls
``run_memory_audit`` / the ``audit_memory`` MCP tool (both of which mutate the
issue ledger and ``memory_events``); it only calls the SELECT-only helpers:

  - ``memory_audit.list_memory_audit_issues``  (open audit issues)
  - ``lazy_enrichment.detect_near_duplicates`` (near-duplicate observations)
  - ``lazy_enrichment.detect_contradictions``  (opposing-predicate claims)
  - ``lazy_enrichment.detect_stale_entities``  (entities idle N days)

Usage:
    memory-lint [DB_PATH] [--status open] [--limit N] [--stale-days N]

DB_PATH defaults to the resolved memory DB path (db_utils.DB_PATH /
$SQLITE_MEMORY_DB). Exit 0 on success.
"""

from __future__ import annotations

import argparse
import os
import sqlite3
import sys

# Make the repo root importable whether invoked as ``bin/memory-lint`` or via an
# absolute path / symlink.
_REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if _REPO_ROOT not in sys.path:
    sys.path.insert(0, _REPO_ROOT)

from db_utils import DB_PATH  # noqa: E402
from lazy_enrichment import (  # noqa: E402
    detect_contradictions,
    detect_near_duplicates,
    detect_stale_entities,
)
from memory_audit import list_memory_audit_issues  # noqa: E402


def open_readonly(db_path: str) -> sqlite3.Connection:
    """Open ``db_path`` strictly read-only.

    Two independent guards make any write impossible:
      1. SQLite URI ``mode=ro`` — the OS-level handle is opened read-only.
      2. ``PRAGMA query_only=ON`` — the connection rejects any write statement.
    """
    if not os.path.exists(db_path):
        raise SystemExit(f"memory-lint: database not found: {db_path}")
    conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
    conn.row_factory = sqlite3.Row
    conn.execute("PRAGMA query_only=ON;")
    return conn


def _print_header(title: str) -> None:
    print(f"\n=== {title} ===")


def collect_report(
    conn: sqlite3.Connection,
    *,
    status: str = "open",
    limit: int = 100,
    stale_days: int = 90,
) -> dict:
    """Run all SELECT-only detectors against ``conn``. No writes."""
    audit = list_memory_audit_issues(conn, status=status, limit=limit)
    return {
        "audit": audit,
        "near_duplicates": detect_near_duplicates(conn),
        "contradictions": detect_contradictions(conn),
        "stale_entities": detect_stale_entities(conn, staleness_days=stale_days),
    }


def render_report(report: dict, *, status: str) -> None:
    audit = report["audit"]
    issues = audit.get("issues", [])
    _print_header(f"AUDIT ISSUES ({status}): {len(issues)}")
    if not issues:
        print("  (none)")
    for it in issues:
        print(
            f"  [{it.get('severity', '?')}] {it.get('issue_type', '?')} "
            f"{it.get('subject_kind', '?')}:{it.get('subject_ref', '?')} "
            f"(last_detected={it.get('last_detected_at', '?')})"
        )

    dups = report["near_duplicates"]
    _print_header(f"NEAR-DUPLICATES: {len(dups)}")
    if not dups:
        print("  (none)")
    for d in dups:
        print(
            f"  {d['entity']} (#{d['entity_id']}) obs {d['obs_a_id']}~{d['obs_b_id']} "
            f"jaccard={d['jaccard']}"
        )
        print(f"    a: {d['text_a']}")
        print(f"    b: {d['text_b']}")

    contras = report["contradictions"]
    _print_header(f"CONTRADICTIONS: {len(contras)}")
    if not contras:
        print("  (none)")
    for c in contras:
        print(
            f"  entity #{c['entity_id']} '{c['subject']}' "
            f"{c['predicate_a']} vs {c['predicate_b']} -> '{c['object']}' "
            f"(claims {c['claim_a']} / {c['claim_b']})"
        )

    stale = report["stale_entities"]
    _print_header(f"STALE ENTITIES: {len(stale)}")
    if not stale:
        print("  (none)")
    for s in stale:
        print(
            f"  #{s['entity_id']} {s['name']} [{s['entity_type']}] "
            f"project={s['project']} last_updated={s['last_updated']} "
            f"last_accessed={s['last_accessed']}"
        )

    total = len(issues) + len(dups) + len(contras) + len(stale)
    _print_header("SUMMARY")
    print(
        f"  audit_issues={len(issues)} near_duplicates={len(dups)} "
        f"contradictions={len(contras)} stale_entities={len(stale)} total={total}"
    )


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(
        prog="memory-lint",
        description="Read-only memory health/lint report (writes nothing).",
    )
    parser.add_argument(
        "db_path",
        nargs="?",
        default=DB_PATH,
        help=f"path to memory.db (default: {DB_PATH})",
    )
    parser.add_argument(
        "--status",
        default="open",
        help="audit issue status to list (default: open)",
    )
    parser.add_argument(
        "--limit",
        type=int,
        default=100,
        help="max audit issues to list (default: 100)",
    )
    parser.add_argument(
        "--stale-days",
        type=int,
        default=90,
        help="staleness threshold in days (default: 90)",
    )
    args = parser.parse_args(argv)

    print(f"memory-lint: read-only report for {args.db_path}")
    conn = open_readonly(args.db_path)
    try:
        report = collect_report(
            conn,
            status=args.status,
            limit=args.limit,
            stale_days=args.stale_days,
        )
        render_report(report, status=args.status)
    finally:
        conn.close()
    return 0


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