#!/usr/bin/env python3
# Author: Tom Sapletta · https://tom.sapletta.com
# Part of the ifURI solution.

"""Invoice audit flow over urirun node routes.

This is deliberately read-only. It calls:

1. ``fs://host/duplicates/query/find`` for exact duplicate detection.
2. ``ocr://host/document/query/batch`` for text extraction.
3. Local CSV/JSON/Markdown report generation.
"""

from __future__ import annotations

import argparse
import csv
import json
import time
import urllib.request
from collections import Counter, defaultdict
from dataclasses import dataclass
from pathlib import Path
from typing import Any

DEFAULT_NODE_URL = "http://192.168.188.201:8765"
DEFAULT_ROOT = "/home/tom/Downloads/2026/5"
DEFAULT_EXTENSIONS = "pdf,png,jpg,jpeg"

CATEGORY_RULES: list[tuple[str, list[str]]] = [
    ("windsurf", ["windsurf", "codeium"]),
    ("ai-saas", ["openai", "anthropic", "claude", "gemini", "midjourney", "elevenlabs", "replicate", "perplexity"]),
    ("github", ["github"]),
    ("saas", ["stripe", "paddle", "subscription", "notion", "figma", "canva", "zoom", "atlassian", "slack", "cursor"]),
    ("domains-hosting", ["namecheap", "cloudflare", "hetzner", "ovh", "hosting", "domain", "domena", "serwer"]),
    ("allegro", ["allegro"]),
    ("payments", ["paypal", "payu", "przelewy24", "tpay", "payment"]),
    ("bank-finance", ["bank", "wise", "revolut", "credit", "card", "konto", "płatność", "platnosc"]),
    ("ksef-gov", ["ksef", "ministerstwo finansów", "ministerstwo finansow", "gov.pl", "e-urzad"]),
    ("retail-shopping", ["amazon", "x-kom", "xkom", "morele", "media expert", "komputronik"]),
    ("email-assets", ["envato", "freepik", "adobe stock", "depositphotos", "shutterstock"]),
    ("poland", ["faktura vat", "nip", "regon", "polska", "sprzedawca"]),
]


@dataclass
class NodeClient:
    base_url: str
    timeout: int = 180

    def run(self, uri: str, payload: dict[str, Any]) -> dict[str, Any]:
        body = json.dumps({"uri": uri, "payload": payload}).encode("utf-8")
        req = urllib.request.Request(
            self.base_url.rstrip("/") + "/run",
            data=body,
            headers={"Content-Type": "application/json"},
            method="POST",
        )
        with urllib.request.urlopen(req, timeout=self.timeout) as response:
            return json.loads(response.read().decode("utf-8"))


def route_value(envelope: dict[str, Any]) -> dict[str, Any]:
    """Return the route function value from a node /run envelope."""
    result = envelope.get("result") or {}
    if isinstance(result, dict) and isinstance(result.get("value"), dict):
        return result["value"]
    if isinstance(result, dict) and isinstance(result.get("response"), dict):
        return result["response"]
    return result if isinstance(result, dict) else {}


def rel_path(path: str, root: str) -> str:
    p = Path(path)
    base = Path(root)
    try:
        return str(p.resolve().relative_to(base.resolve()))
    except (OSError, ValueError):
        return str(p)


def classify_document(path: str, text: str) -> tuple[str, str]:
    haystack = f"{path}\n{text}".lower()
    for category, needles in CATEGORY_RULES:
        for needle in needles:
            if needle in haystack:
                return category, needle
    return "unknown", ""


def duplicate_path_set(duplicates: dict[str, Any]) -> set[str]:
    paths: set[str] = set()
    for group in duplicates.get("groups") or []:
        for path in group.get("paths") or []:
            paths.add(str(path))
    return paths


def build_file_rows(ocr: dict[str, Any], duplicates: dict[str, Any], root: str) -> list[dict[str, Any]]:
    dupes = duplicate_path_set(duplicates)
    rows: list[dict[str, Any]] = []
    for item in ocr.get("results") or []:
        absolute = str(item.get("path", ""))
        relative = rel_path(absolute, root)
        text = str(item.get("text") or "")
        category, reason = classify_document(relative, text)
        rows.append({
            "path": relative,
            "category": category,
            "reason": reason,
            "duplicate": relative in dupes,
            "ok": bool(item.get("ok")),
            "backend": item.get("backend", ""),
            "chars": int(item.get("chars") or 0),
            "error": item.get("error", ""),
            "preview": " ".join(text.split())[:240],
        })
    return rows


def summarize_categories(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
    by_category: dict[str, dict[str, Any]] = {}
    for row in rows:
        category = str(row["category"])
        bucket = by_category.setdefault(category, {
            "category": category,
            "file_count": 0,
            "ocr_ok": 0,
            "ocr_failed": 0,
            "duplicate_files": 0,
            "chars_total": 0,
            "top_reasons": Counter(),
            "examples": [],
        })
        bucket["file_count"] += 1
        bucket["ocr_ok"] += 1 if row["ok"] else 0
        bucket["ocr_failed"] += 0 if row["ok"] else 1
        bucket["duplicate_files"] += 1 if row["duplicate"] else 0
        bucket["chars_total"] += int(row["chars"])
        if row["reason"]:
            bucket["top_reasons"][row["reason"]] += 1
        if len(bucket["examples"]) < 3:
            bucket["examples"].append(row["path"])

    summary: list[dict[str, Any]] = []
    for bucket in by_category.values():
        reasons = bucket.pop("top_reasons")
        bucket["top_reasons"] = "; ".join(f"{name}:{count}" for name, count in reasons.most_common(5))
        bucket["examples"] = "; ".join(bucket["examples"])
        summary.append(bucket)
    return sorted(summary, key=lambda row: (-int(row["file_count"]), str(row["category"])))


def write_csv(path: Path, rows: list[dict[str, Any]], fields: list[str]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("w", encoding="utf-8", newline="") as handle:
        writer = csv.DictWriter(handle, fieldnames=fields)
        writer.writeheader()
        for row in rows:
            writer.writerow({field: row.get(field, "") for field in fields})


def write_reports(out_dir: Path, payload: dict[str, Any]) -> dict[str, str]:
    out_dir.mkdir(parents=True, exist_ok=True)
    raw_path = out_dir / "invoice_audit_raw.json"
    files_path = out_dir / "invoice_audit_files.csv"
    categories_path = out_dir / "invoice_audit_categories.csv"
    duplicates_path = out_dir / "invoice_audit_duplicates.csv"
    md_path = out_dir / "invoice_audit_report.md"

    raw_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
    write_csv(
        files_path,
        payload["files"],
        ["path", "category", "reason", "duplicate", "ok", "backend", "chars", "error", "preview"],
    )
    write_csv(
        categories_path,
        payload["categories"],
        ["category", "file_count", "ocr_ok", "ocr_failed", "duplicate_files", "chars_total", "top_reasons", "examples"],
    )
    duplicate_rows = []
    for group in payload["duplicates"].get("groups") or []:
        duplicate_rows.append({
            "sha256": group.get("sha256", ""),
            "size": group.get("size", 0),
            "count": group.get("count", 0),
            "reclaimableBytes": group.get("reclaimableBytes", 0),
            "paths": "; ".join(group.get("paths") or []),
        })
    write_csv(duplicates_path, duplicate_rows, ["sha256", "size", "count", "reclaimableBytes", "paths"])

    lines = [
        "# Invoice Audit",
        "",
        f"- root: `{payload['root']}`",
        f"- files: {len(payload['files'])}",
        f"- duplicate groups: {payload['duplicates'].get('groupCount', 0)}",
        f"- duplicate files: {payload['duplicates'].get('duplicateFiles', 0)}",
        f"- reclaimable bytes: {payload['duplicates'].get('reclaimableBytes', 0)}",
        "",
        "## Categories",
        "",
        "| category | files | ocr ok | ocr failed | duplicates |",
        "| --- | ---: | ---: | ---: | ---: |",
    ]
    for row in payload["categories"]:
        lines.append(
            f"| {row['category']} | {row['file_count']} | {row['ocr_ok']} | "
            f"{row['ocr_failed']} | {row['duplicate_files']} |"
        )
    md_path.write_text("\n".join(lines) + "\n", encoding="utf-8")

    return {
        "raw_json": str(raw_path),
        "files_csv": str(files_path),
        "categories_csv": str(categories_path),
        "duplicates_csv": str(duplicates_path),
        "markdown": str(md_path),
    }


def run_audit(args: argparse.Namespace, client: NodeClient | None = None) -> dict[str, Any]:
    client = client or NodeClient(args.node_url, timeout=args.timeout)
    started = time.time()
    dup_env = client.run(args.fs_uri, {
        "path": args.fs_path,
        "extensions": args.extensions,
        "max_files": args.max_files,
        "recursive": True,
    })
    duplicates = route_value(dup_env)
    if not duplicates.get("ok"):
        raise RuntimeError(f"duplicates route failed: {duplicates.get('error') or dup_env}")

    ocr_env = client.run(args.ocr_uri, {
        "root": args.root,
        "extensions": args.extensions,
        "max_files": args.max_files,
        "max_chars_per_file": args.max_chars_per_file,
        "backend": args.backend,
    })
    ocr = route_value(ocr_env)
    if not ocr.get("ok"):
        raise RuntimeError(f"OCR batch route failed: {ocr.get('error') or ocr_env}")

    files = build_file_rows(ocr, duplicates, args.root)
    categories = summarize_categories(files)
    payload = {
        "ok": True,
        "root": args.root,
        "node_url": args.node_url,
        "elapsed_seconds": round(time.time() - started, 3),
        "duplicates": duplicates,
        "ocr": {k: v for k, v in ocr.items() if k != "results"},
        "files": files,
        "categories": categories,
    }
    payload["reports"] = write_reports(Path(args.output_dir), payload)
    return payload


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description="Run read-only invoice audit over urirun URI routes.")
    parser.add_argument("--node-url", default=DEFAULT_NODE_URL)
    parser.add_argument("--root", default=DEFAULT_ROOT, help="absolute invoice folder on the node")
    parser.add_argument("--fs-path", default=".", help="path under IFURI_FS_ROOT for fs:// duplicate scan")
    parser.add_argument("--extensions", default=DEFAULT_EXTENSIONS)
    parser.add_argument("--max-files", type=int, default=10000)
    parser.add_argument("--max-chars-per-file", type=int, default=1200)
    parser.add_argument("--backend", default="auto")
    parser.add_argument("--timeout", type=int, default=240)
    parser.add_argument("--output-dir", default=".state")
    parser.add_argument("--fs-uri", default="fs://host/duplicates/query/find")
    parser.add_argument("--ocr-uri", default="ocr://host/document/query/batch")
    return parser


def main(argv: list[str] | None = None) -> int:
    args = build_parser().parse_args(argv)
    result = run_audit(args)
    print(json.dumps({
        "ok": True,
        "files": len(result["files"]),
        "categories": len(result["categories"]),
        "duplicateGroups": result["duplicates"].get("groupCount", 0),
        "reports": result["reports"],
    }, ensure_ascii=False, indent=2))
    return 0


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