#!/usr/bin/env python3
"""List changed tracked Markdown without sending entire documents to an agent.

scan never advances the checkpoint. ack records only reviewed, unchanged hashes.
State and scan manifests belong outside Git; reports contain no source bodies.
"""
import argparse
import hashlib
import json
import subprocess
import time
from pathlib import Path


def digest(path):
    return hashlib.sha256(path.read_bytes()).hexdigest()


def atomic_json(path, value):
    path.parent.mkdir(parents=True, exist_ok=True)
    tmp = path.with_suffix(path.suffix + '.tmp')
    tmp.write_text(json.dumps(value, ensure_ascii=False, indent=2) + '\n')
    tmp.replace(path)


def eligible(name):
    return name.lower().endswith('.md') and not any(x in Path(name).parts for x in (
        'upstream', 'node_modules', 'vendor', 'daily-optimization', '.git'))


def scan(root, state, limit):
    names = subprocess.check_output(['git', 'ls-files', '-z'], cwd=root).decode().split('\0')
    rows = []
    for name in names:
        path = root / name
        if not eligible(name) or path.is_symlink() or not path.is_file():
            continue
        sha = digest(path)
        if state.get(name) == sha:
            continue
        size = path.stat()
        rows.append(dict(path=name, sha256=sha, bytes=size.st_size, mtime=size.st_mtime,
                         kind='skill' if name.endswith('/SKILL.md') else 'knowledge',
                         priority=(0 if time.time() - size.st_mtime < 86400 else 2)
                         + (0 if 'thread-records/' in name else 1)))
    rows.sort(key=lambda row: (row['priority'], -row['mtime'], row['path']))
    chosen = rows[:limit]
    if limit >= 4 and len(rows) > limit:
        oldest = min(rows, key=lambda row: (row['mtime'], row['path']))
        if oldest not in chosen:
            chosen[-1] = oldest
    return dict(pending=len(rows), selected=chosen, deferred=max(0, len(rows)-limit),
                scope='tracked Markdown; changed content hashes; untracked and upstream excluded')


def acknowledge(root, state, manifest, paths):
    selected = {row['path']: row for row in manifest['selected']}
    for name in paths:
        if name not in selected:
            raise ValueError('Not in scan: ' + name)
        path = root / name
        if path.is_symlink() or not path.is_file() or digest(path) != selected[name]['sha256']:
            raise ValueError('Changed since scan; rescan before acknowledging: ' + name)
    return dict(state, **{name: selected[name]['sha256'] for name in paths})


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('action', choices=['scan', 'ack'])
    parser.add_argument('--root', type=Path, default=Path.cwd())
    parser.add_argument('--state', type=Path, required=True)
    parser.add_argument('--manifest', type=Path, required=True)
    parser.add_argument('--limit', type=int, default=12)
    parser.add_argument('--path', action='append', default=[])
    args = parser.parse_args()
    root = args.root.resolve()
    if args.limit < 1:
        parser.error('--limit must be positive')
    for local in (args.state, args.manifest):
        if local.resolve().is_relative_to(root):
            parser.error('State and manifest must stay outside repository')
    state = json.loads(args.state.read_text()) if args.state.exists() else {}
    if args.action == 'scan':
        result = scan(root, state, args.limit)
        atomic_json(args.manifest, result)
        print(json.dumps(result, ensure_ascii=False, indent=2))
    else:
        if not args.path:
            parser.error('ack requires explicit --path for reviewed files')
        result = acknowledge(root, state, json.loads(args.manifest.read_text()), args.path)
        atomic_json(args.state, result)
        print(json.dumps(dict(acknowledged=len(args.path))))


if __name__ == '__main__':
    main()
