#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
Phase 2B workspace registry / hash audit.

Generates:
    14_initial_engine_outputs/audit/workspace_registry.csv
    14_initial_engine_outputs/audit/workspace_hash_manifest.sha256
    14_initial_engine_outputs/audit/workspace_validation_report.txt

Validation checks:
1. Every discovered source input has JSON, CSV, and XLSX outputs.
2. Combined flat TXT/CSV/XLSX outputs exist.
3. JSON output contains all 29 initialed suras.
4. JSON letter totals are non-negative integers.
5. CSV row counts match JSON verse counts.
6. XLSX row counts match JSON verse counts.

Note:
This audit records XLSX hashes for file-integrity visibility, but
reproducibility verification should use verify_repro.ps1 and compare only
canonical CSV/JSON/TXT outputs.
"""

from __future__ import annotations

import argparse
import csv
import hashlib
import json
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Iterable

from openpyxl import load_workbook


DEFAULT_EXCLUDE_DIRS = {
    ".git",
    ".venv",
    "venv",
    "__pycache__",
    ".mypy_cache",
    ".pytest_cache",
    ".ruff_cache",
    ".vscode",
}

OUTPUT_DIR_NAME = "14_initial_engine_outputs"

INITIALS_BY_SURA = {
    2:  ["ا","ل","م"],  3:  ["ا","ل","م"],  7:  ["ا","ل","م","ص"],
    10: ["ا","ل","ر"], 11: ["ا","ل","ر"], 12: ["ا","ل","ر"],
    13: ["ا","ل","م","ر"], 14: ["ا","ل","ر"], 15: ["ا","ل","ر"],
    19: ["ك","ه","ي","ع","ص"], 20: ["ط","ه"],
    26: ["ط","س","م"], 27: ["ط","س"], 28: ["ط","س","م"],
    29: ["ا","ل","م"], 30: ["ا","ل","م"], 31: ["ا","ل","م"],
    32: ["ا","ل","م"], 36: ["ي","س"], 38: ["ص"],
    40: ["ح","م"], 41: ["ح","م"], 42: ["ح","م","ع","س","ق"],
    43: ["ح","م"], 44: ["ح","م"], 45: ["ح","م"],
    46: ["ح","م"], 50: ["ق"], 68: ["ن"],
}
EXPECTED_SURAS = sorted(INITIALS_BY_SURA.keys())

ARABIC_LETTERS = [
    "ا","ب","ت","ث","ج","ح","خ","د","ذ","ر","ز","س","ش","ص",
    "ض","ط","ظ","ع","غ","ف","ق","ك","ل","م","ن","ه","و","ي"
]


@dataclass
class FileEntry:
    rel_path: str
    size_bytes: int
    sha256: str


def sha256_file(path: Path, chunk_size: int = 1024 * 1024) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(chunk_size), b""):
            digest.update(chunk)
    return digest.hexdigest()


def iter_files(root: Path, exclude_dirs: set[str]) -> Iterable[Path]:
    for path in sorted(root.rglob("*")):
        if not path.is_file():
            continue
        rel = path.relative_to(root)
        if any(part in exclude_dirs for part in rel.parts):
            continue
        yield path


def build_registry(root: Path, exclude_dirs: set[str]) -> list[FileEntry]:
    return [
        FileEntry(
            rel_path=path.relative_to(root).as_posix(),
            size_bytes=path.stat().st_size,
            sha256=sha256_file(path),
        )
        for path in iter_files(root, exclude_dirs)
    ]


def write_registry_csv(entries: list[FileEntry], out_csv: Path) -> None:
    out_csv.parent.mkdir(parents=True, exist_ok=True)
    with out_csv.open("w", encoding="utf-8", newline="") as handle:
        writer = csv.writer(handle)
        writer.writerow(["path", "size_bytes", "sha256"])
        for entry in entries:
            writer.writerow([entry.rel_path, entry.size_bytes, entry.sha256])


def write_hash_manifest(entries: list[FileEntry], out_manifest: Path) -> None:
    out_manifest.parent.mkdir(parents=True, exist_ok=True)
    with out_manifest.open("w", encoding="utf-8") as handle:
        for entry in entries:
            handle.write(f"{entry.sha256}  {entry.rel_path}\n")


def canonicalize_mushaf_slug(value: str) -> str:
    slug = str(value).strip().lower().replace(" ", "")
    if slug.endswith("_normalized"):
        slug = slug[: -len("_normalized")]
    elif slug.endswith("normalized"):
        slug = slug[: -len("normalized")]
    return slug.rstrip("_-")


def slug_from_filename(path: Path) -> str:
    base = path.name
    lower = base.lower()

    if lower.endswith("_letter_matrix.csv"):
        base = base[: -len("_LETTER_MATRIX.csv")]
    elif lower.endswith(".csv"):
        base = base[:-len(".csv")]
    elif lower.endswith(".xlsx"):
        base = base[:-len(".xlsx")]

    lower = base.lower()
    if "_ornamentation__normalized" in lower:
        base = base[: lower.index("_ornamentation__normalized")]

    if base.lower().endswith("_initials"):
        base = base[: -len("_initials")]
    elif base.lower().endswith(" initials"):
        base = base[: -len(" initials")]

    return canonicalize_mushaf_slug(base)


def discover_source_files(root: Path) -> list[Path]:
    excluded_names = {
        "14_initial_engine_flat.txt",
        "14_initial_engine_flat.xlsx",
        "14_initial_engine_flat.csv",
    }

    candidates = []
    for path in sorted(root.iterdir()):
        if not path.is_file():
            continue
        if path.name.startswith("~$"):
            continue
        if path.name.lower() in excluded_names:
            continue
        if path.suffix.lower() in {".csv", ".xlsx"}:
            candidates.append(path)
    return candidates


def count_csv_rows(path: Path) -> int:
    with path.open("r", encoding="utf-8-sig", newline="") as handle:
        reader = csv.DictReader(handle)
        return sum(1 for _ in reader)


def count_xlsx_rows(path: Path) -> int:
    wb = load_workbook(path, read_only=True, data_only=True)
    ws = wb.active
    count = 0
    for _row in ws.iter_rows(min_row=2, values_only=True):
        count += 1
    wb.close()
    return count


def validate_json(json_path: Path) -> tuple[list[str], int]:
    checks = []
    payload = json.loads(json_path.read_text(encoding="utf-8"))

    suras = payload.get("suras", {})
    missing = [s for s in EXPECTED_SURAS if str(s) not in suras]
    if missing:
        checks.append(f"JSON_MISSING_INITIAL_SURAS: {json_path.name} missing={missing}")

    total_rows = 0
    for sura in EXPECTED_SURAS:
        sura_obj = suras.get(str(sura), {})
        verses = sura_obj.get("verses", [])
        total_rows += len(verses)

        expected_initials = INITIALS_BY_SURA[sura]
        actual_initials = sura_obj.get("initials", [])
        if actual_initials != expected_initials:
            checks.append(
                f"JSON_INITIALS_MISMATCH: {json_path.name} sura={sura} expected={expected_initials} actual={actual_initials}"
            )

        verse_nums = []
        for verse_obj in verses:
            verse_nums.append(verse_obj.get("verse"))
            letters = verse_obj.get("letters", {})
            for letter in ARABIC_LETTERS:
                val = letters.get(letter, 0)
                if not isinstance(val, int) or val < 0:
                    checks.append(
                        f"JSON_INVALID_LETTER_COUNT: {json_path.name} sura={sura} verse={verse_obj.get('verse')} letter={letter} value={val!r}"
                    )

        if verse_nums != sorted(set(verse_nums)):
            checks.append(f"JSON_VERSE_ORDER_OR_DUPLICATE: {json_path.name} sura={sura}")

    return checks, total_rows


def collect_pipeline_checks(root: Path) -> list[str]:
    checks: list[str] = []

    out_dir = root / OUTPUT_DIR_NAME
    json_dir = out_dir / "json"
    csv_dir = out_dir / "csv"
    excel_dir = out_dir / "excel"

    source_files = discover_source_files(root)
    source_slugs = sorted({slug_from_filename(path) for path in source_files})

    if not source_slugs:
        checks.append("NO_SOURCE_INPUTS_FOUND")
        return checks

    for slug in source_slugs:
        expected_json = json_dir / f"{slug}_initial_counts.json"
        expected_csv = csv_dir / f"{slug}_initial_counts.csv"
        expected_xlsx = excel_dir / f"{slug}_initial_counts.xlsx"

        for target in [expected_json, expected_csv, expected_xlsx]:
            if not target.exists():
                checks.append(f"MISSING_OUTPUT: {target.relative_to(root).as_posix()}")

        if expected_json.exists():
            json_checks, json_rows = validate_json(expected_json)
            checks.extend(json_checks)

            if expected_csv.exists():
                csv_rows = count_csv_rows(expected_csv)
                if csv_rows != json_rows:
                    checks.append(
                        f"JSON_CSV_ROW_MISMATCH: {slug} json={json_rows} csv={csv_rows}"
                    )

            if expected_xlsx.exists():
                xlsx_rows = count_xlsx_rows(expected_xlsx)
                if xlsx_rows != json_rows:
                    checks.append(
                        f"JSON_XLSX_ROW_MISMATCH: {slug} json={json_rows} xlsx={xlsx_rows}"
                    )

    flat_files = [
        out_dir / "14_initial_engine_flat.txt",
        out_dir / "14_initial_engine_flat.csv",
        out_dir / "14_initial_engine_flat.xlsx",
    ]
    for target in flat_files:
        if not target.exists():
            checks.append(f"MISSING_COMBINED_FLAT_OUTPUT: {target.relative_to(root).as_posix()}")

    return checks


def write_validation_report(
    entries: list[FileEntry],
    checks: list[str],
    root: Path,
    out_report: Path,
    exclude_dirs: set[str],
    strict_duplicates: bool,
) -> str:
    out_report.parent.mkdir(parents=True, exist_ok=True)

    hash_map: dict[str, list[str]] = {}
    total_size = 0
    for entry in entries:
        total_size += entry.size_bytes
        hash_map.setdefault(entry.sha256, []).append(entry.rel_path)

    duplicate_groups = [paths for paths in hash_map.values() if len(paths) > 1]

    status = "PASS" if not checks else "FAIL"
    if strict_duplicates and duplicate_groups:
        status = "FAIL"

    now = datetime.now(timezone.utc).isoformat()

    lines = [
        "PHASE 2B WORKSPACE VALIDATION REPORT",
        "=" * 37,
        f"Generated UTC: {now}",
        f"Workspace: {root.as_posix()}",
        f"Status: {status}",
        "",
        "Summary",
        "-------",
        f"Files scanned: {len(entries)}",
        f"Total size bytes: {total_size}",
        f"Duplicate hash groups: {len(duplicate_groups)}",
        f"Strict duplicates mode: {'ON' if strict_duplicates else 'OFF'}",
        f"Excluded directories: {', '.join(sorted(exclude_dirs))}",
        "",
        "Validation Checks",
        "-----------------",
    ]

    if checks:
        lines.extend(f"- {item}" for item in checks)
    else:
        lines.append("- OK: All validation checks passed.")

    lines.extend([
        "",
        "Duplicate Hash Groups (if any)",
        "------------------------------",
    ])

    if duplicate_groups:
        for i, group in enumerate(duplicate_groups, start=1):
            lines.append(f"Group {i}:")
            for path in group:
                lines.append(f"  * {path}")
    else:
        lines.append("- None")

    lines.extend([
        "",
        "Reproducibility Note",
        "--------------------",
        "Canonical reproducibility is verified separately by verify_repro.ps1 using CSV, JSON, and TXT outputs.",
        "XLSX files are included in the registry for file-integrity visibility, but are not used as canonical deterministic artifacts.",
    ])

    out_report.write_text("\n".join(lines) + "\n", encoding="utf-8")
    return status


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Generate Phase 2B workspace file registry, SHA256 hash manifest, and validation report."
    )
    parser.add_argument(
        "--root",
        type=Path,
        default=Path(__file__).resolve().parent,
        help="Workspace root directory to audit.",
    )
    parser.add_argument(
        "--output-dir",
        type=Path,
        default=Path(OUTPUT_DIR_NAME) / "audit",
        help="Output directory for generated audit artifacts.",
    )
    parser.add_argument(
        "--exclude-dir",
        action="append",
        default=[],
        help="Directory name to exclude. Can be supplied multiple times.",
    )
    parser.add_argument(
        "--strict-duplicates",
        action="store_true",
        help="Fail validation when duplicate hash groups are present.",
    )
    return parser.parse_args()


def main() -> int:
    args = parse_args()
    root = args.root.resolve()

    exclude_dirs = set(DEFAULT_EXCLUDE_DIRS)
    exclude_dirs.update(args.exclude_dir)

    output_dir = args.output_dir
    if not output_dir.is_absolute():
        output_dir = root / output_dir
    output_dir.mkdir(parents=True, exist_ok=True)

    registry_path = output_dir / "workspace_registry.csv"
    hash_manifest_path = output_dir / "workspace_hash_manifest.sha256"
    report_path = output_dir / "workspace_validation_report.txt"

    entries = build_registry(root, exclude_dirs)
    write_registry_csv(entries, registry_path)
    write_hash_manifest(entries, hash_manifest_path)

    checks = collect_pipeline_checks(root)
    status = write_validation_report(
        entries=entries,
        checks=checks,
        root=root,
        out_report=report_path,
        exclude_dirs=exclude_dirs,
        strict_duplicates=args.strict_duplicates,
    )

    print(f"STATUS={status}")
    print(f"REGISTRY={registry_path.relative_to(root).as_posix()}")
    print(f"HASH_MANIFEST={hash_manifest_path.relative_to(root).as_posix()}")
    print(f"VALIDATION_REPORT={report_path.relative_to(root).as_posix()}")

    return 0 if status == "PASS" else 1


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