#!/usr/bin/env python3
"""Cyberdelia FOIA Intake Indexer v0.1.0.

Dependency-free archival intake utility. Recursively inventories a directory,
computes SHA-256 hashes, groups exact duplicates, and writes JSON + CSV outputs.
It does not modify source files.
"""
from __future__ import annotations

import argparse
import csv
import hashlib
import json
import mimetypes
from collections import defaultdict
from datetime import datetime, timezone
from pathlib import Path

VERSION = "0.1.0"
CHUNK = 1024 * 1024


def sha256_file(path: Path) -> str:
    h = hashlib.sha256()
    with path.open("rb") as fh:
        for chunk in iter(lambda: fh.read(CHUNK), b""):
            h.update(chunk)
    return h.hexdigest()


def record_for(root: Path, path: Path) -> dict:
    st = path.stat()
    mime, _ = mimetypes.guess_type(path.name)
    return {
        "path": path.relative_to(root).as_posix(),
        "bytes": st.st_size,
        "sha256": sha256_file(path),
        "mime_guess": mime or "application/octet-stream",
        "mtime_utc": datetime.fromtimestamp(st.st_mtime, tz=timezone.utc).isoformat(),
        "extension": path.suffix.lower(),
    }


def scan(root: Path, ignore: set[str]) -> list[dict]:
    out: list[dict] = []
    for path in sorted(root.rglob("*")):
        if not path.is_file():
            continue
        rel_parts = set(path.relative_to(root).parts)
        if rel_parts & ignore:
            continue
        out.append(record_for(root, path))
    return out


def duplicate_groups(records: list[dict]) -> list[dict]:
    groups: dict[str, list[str]] = defaultdict(list)
    for rec in records:
        groups[rec["sha256"]].append(rec["path"])
    return [
        {"sha256": digest, "count": len(paths), "paths": paths}
        for digest, paths in sorted(groups.items())
        if len(paths) > 1
    ]


def write_csv(path: Path, records: list[dict]) -> None:
    fields = ["path", "bytes", "sha256", "mime_guess", "mtime_utc", "extension"]
    with path.open("w", newline="", encoding="utf-8") as fh:
        w = csv.DictWriter(fh, fieldnames=fields)
        w.writeheader()
        w.writerows(records)


def main() -> int:
    p = argparse.ArgumentParser(description="Inventory archival/FOIA intake without modifying source files.")
    p.add_argument("root", type=Path, help="Directory to inventory")
    p.add_argument("--out", type=Path, default=Path("cyberdelia-intake-output"), help="Output directory")
    p.add_argument("--ignore", action="append", default=[], help="Directory/file component to ignore; repeatable")
    args = p.parse_args()

    root = args.root.resolve()
    if not root.is_dir():
        p.error(f"not a directory: {root}")

    out = args.out.resolve()
    out.mkdir(parents=True, exist_ok=True)
    ignore = set(args.ignore) | {out.name, ".git", "__pycache__"}
    records = scan(root, ignore)
    dups = duplicate_groups(records)
    total_bytes = sum(r["bytes"] for r in records)

    manifest = {
        "tool": "Cyberdelia FOIA Intake Indexer",
        "version": VERSION,
        "generated_utc": datetime.now(timezone.utc).isoformat(),
        "source_root": str(root),
        "file_count": len(records),
        "total_bytes": total_bytes,
        "duplicate_group_count": len(dups),
        "records": records,
        "exact_duplicate_groups": dups,
        "notes": [
            "SHA-256 identifies byte-for-byte equality, not semantic equality.",
            "mtime_utc is filesystem metadata and is not treated as document creation time.",
            "mime_guess is extension-based and should not be treated as forensic file typing.",
            "Source files are read only; this tool does not alter them."
        ]
    }

    (out / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
    write_csv(out / "inventory.csv", records)
    (out / "duplicates.json").write_text(json.dumps(dups, indent=2) + "\n", encoding="utf-8")
    print(f"indexed {len(records)} files / {total_bytes} bytes / {len(dups)} exact duplicate groups")
    print(out)
    return 0


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