#!/usr/bin/env python3
"""Cobertura do corpus sucesu_analise contra o manifest do V2.

Uso:
  coverage.py [--strict] [--docs] [--complete] [--json]

  --strict    falha em erro de integridade do manifest (órfão, duplicado, ausente)
  --docs      exige que toda unidade esteja reivindicada por um documento
  --complete  exige que toda unidade esteja com status concluido

Sem flags, apenas relata. Saída legível; --json emite JSON.
"""

from __future__ import annotations

import argparse
import json
import os
import re
import sys
from collections import defaultdict

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
V2_ROOT = os.path.normpath(os.path.join(ROOT, "..", "sucesu-cms", "V2"))
MANIFEST = os.path.join(ROOT, "00_inventario", "manifest.json")
SKIP_DIRS = {"_contrato", "00_inventario", "tools", "_indices", ".git", "v7_implementation"}

CITATION = re.compile(r"[A-Za-z0-9_./-]+\.(?:php|blade\.php|js|css|json|md|sh):\d+")


def norm_prefix(prefix: str) -> str:
    """Docs usam caminhos relativos a sucesu-cms/ (V2/...); o manifest usa V2/ como raiz."""
    return prefix[3:] if prefix.startswith("V2/") else prefix


def load_manifest() -> dict:
    with open(MANIFEST, encoding="utf-8") as fh:
        return json.load(fh)


def parse_front_matter(path: str) -> dict:
    with open(path, encoding="utf-8") as fh:
        text = fh.read()
    if not text.startswith("---\n"):
        return {}
    end = text.find("\n---", 4)
    if end == -1:
        return {}
    meta: dict = {}
    key = None
    for line in text[4:end].splitlines():
        if not line.strip() or line.lstrip().startswith("#"):
            continue
        if line.startswith("  - ") or line.startswith("- "):
            if key:
                meta.setdefault(key, [])
                if isinstance(meta[key], list):
                    meta[key].append(line.split("- ", 1)[1].strip())
            continue
        if ":" in line:
            key, value = line.split(":", 1)
            key = key.strip()
            value = value.strip()
            if value.startswith("[") and value.endswith("]"):
                inner = value[1:-1].strip()
                meta[key] = [v.strip() for v in inner.split(",") if v.strip()] if inner else []
            elif value == "":
                meta[key] = []
            else:
                meta[key] = value.strip('"\'')
    return meta


def iter_docs() -> list[str]:
    docs = []
    for dirpath, dirnames, filenames in os.walk(ROOT):
        rel = os.path.relpath(dirpath, ROOT)
        top = rel.split(os.sep)[0]
        if top in SKIP_DIRS:
            dirnames[:] = []
            continue
        for name in filenames:
            if name.endswith(".md"):
                docs.append(os.path.join(dirpath, name))
    return docs


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--strict", action="store_true")
    ap.add_argument("--docs", action="store_true")
    ap.add_argument("--complete", action="store_true")
    ap.add_argument("--json", action="store_true")
    args = ap.parse_args()

    manifest = load_manifest()
    units = manifest["units"]
    errors: list[str] = []
    assigned: dict[str, str] = {}

    if not manifest["totals"]["files"] == sum(u["count"] for u in units.values()):
        errors.append("manifest: total de arquivos diverge da soma das unidades")
    if manifest["totals"]["units"] != len(units):
        errors.append("manifest: total de unidades diverge")

    for unit, info in units.items():
        for path in info["files"]:
            if path in assigned:
                errors.append(f"duplicado: {path} em {assigned[path]} e {unit}")
            assigned[path] = unit
            if not os.path.exists(os.path.join(V2_ROOT, path)):
                errors.append(f"ausente no disco: {path} ({unit})")

    if len(assigned) != manifest["totals"]["files"]:
        errors.append(
            f"manifest: {len(assigned)} arquivos atribuídos, esperado {manifest['totals']['files']}"
        )

    claimed: dict[str, list[dict]] = defaultdict(list)
    for path in iter_docs():
        meta = parse_front_matter(path)
        if not meta.get("id"):
            continue
        rel = os.path.relpath(path, ROOT)
        doc = {
            "file": rel,
            "id": meta["id"],
            "tipo": meta.get("tipo", ""),
            "status": meta.get("status", ""),
            "unidades": meta.get("unidades") or [],
            "fontes": meta.get("fontes") or [],
            "citacoes": len(CITATION.findall(open(path, encoding="utf-8").read())),
        }
        # Documentos transversais (matriz/reimplementacao) com unidades: [] não
        # reivindicam cobertura. Se listarem unidades (ex.: GRP-INDEX), reivindicam
        # normalmente; docs de unidade sem lista caem no fallback id -> unidade.
        if doc["unidades"]:
            claimed_units = doc["unidades"]
        elif doc["tipo"] in {"matriz", "reimplementacao"}:
            claimed_units = []
        else:
            claimed_units = [doc["id"]]
        for unit in claimed_units:
            if unit not in units:
                errors.append(f"{rel}: unidade desconhecida {unit}")
                continue
            claimed[unit].append(doc)

    covered_files: set[str] = set()
    unit_report = {}
    for unit, info in units.items():
        docs = claimed.get(unit, [])
        prefixes = [norm_prefix(p) for d in docs for p in d["fontes"]]
        files = info["files"]
        covered = [f for f in files if any(f.startswith(p) for p in prefixes)]
        covered_files.update(covered)
        unit_report[unit] = {
            "arquivos": len(files),
            "loc": info["loc"],
            "docs": [d["file"] for d in docs],
            "status": sorted({d["status"] for d in docs}) or ["sem doc"],
            "cobertos": len(covered),
        }

    uncovered_units = [u for u, r in unit_report.items() if not r["docs"]]
    incomplete = [u for u, r in unit_report.items() if "concluido" not in r["status"]]
    not_fully_covered = [u for u, r in unit_report.items()
                         if r["docs"] and r["cobertos"] != r["arquivos"]]

    if args.docs and uncovered_units:
        errors.append(f"{len(uncovered_units)} unidades sem documento")
    if args.complete and incomplete:
        errors.append(f"{len(incomplete)} unidades sem status concluido")

    result = {
        "v2_files": manifest["totals"]["files"],
        "units": len(units),
        "docs": sum(len(d) for d in claimed.values()),
        "units_with_docs": len(units) - len(uncovered_units),
        "files_covered": len(covered_files),
        "units_uncovered": uncovered_units,
        "units_incomplete": incomplete,
        "units_partial_coverage": not_fully_covered,
        "errors": errors,
        "report": unit_report,
    }

    if args.json:
        print(json.dumps(result, ensure_ascii=False, indent=2))
    else:
        print(f"V2: {result['v2_files']} arquivos | {result['units']} unidades")
        print(f"Docs: {result['docs']} | unidades com doc: {result['units_with_docs']}/{result['units']}")
        print(f"Arquivos cobertos por fontes: {result['files_covered']}/{result['v2_files']}")
        if uncovered_units:
            print("\nSem documento:")
            for unit in uncovered_units[:15]:
                print(f"  - {unit}")
            if len(uncovered_units) > 15:
                print(f"  ... e mais {len(uncovered_units) - 15}")
        if not_fully_covered:
            print("\nCobertura parcial (prefixos de fontes incompletos):")
            for unit in not_fully_covered:
                r = unit_report[unit]
                print(f"  - {unit}: {r['cobertos']}/{r['arquivos']}")
        if errors:
            print("\nERROS:")
            for err in errors:
                print(f"  - {err}")
        if not errors:
            print("\nIntegridade OK")

    return 1 if errors else 0


if __name__ == "__main__":
    sys.exit(main())
