
from __future__ import annotations

from collections import defaultdict
import json
import re
import sys
from pathlib import Path
from typing import Dict, List, Optional, Tuple

from openpyxl import load_workbook

import hash_registry
from _pipeline_utils import _generated_timestamp, m4_m6_abjad_digits

if hasattr(sys, "set_int_max_str_digits"):
    sys.set_int_max_str_digits(0)

METRIC_ORDER = ["M1", "M2", "M3", "M4", "M5", "M6", "M7"]
EXPECTED_MUSHAFS = [
    "Bazzi",
    "Doori",
    "Hafs",
    "Qaloon",
    "Qumball",
    "Shouba",
    "Soosi",
    "Submission",
    "The_Criterion",
    "Warsh",
]


def _to_int(value: object) -> Optional[int]:
    """Convert supported cell values to int; return None when not numeric."""
    if value is None:
        return None

    if isinstance(value, bool):
        return None

    if isinstance(value, int):
        return value

    if isinstance(value, float):
        return int(value) if value.is_integer() else None

    if isinstance(value, str):
        stripped = value.strip()
        if not stripped:
            return None
        if stripped.isdigit():
            return int(stripped)

    return None


def _mushaf_name_from_file(file_path: Path) -> str:
    """Extract mushaf name from '<Name>_7Metrics.xlsx'."""
    return file_path.stem.replace("_7Metrics", "")


def _short_metric_name(metric_name: str) -> str:
    """Convert metric header like 'M1_LetterCount' to 'M1'."""
    return metric_name.split("_", 1)[0]


def _collect_excel_files(input_dir: Path) -> List[Path]:
    files = list(input_dir.glob("*_7Metrics.xlsx"))
    if not files:
        raise FileNotFoundError("No files matching '*_7Metrics.xlsx' were found.")

    by_mushaf = {_mushaf_name_from_file(path): path for path in files}
    missing = [name for name in EXPECTED_MUSHAFS if name not in by_mushaf]
    extras = sorted(name for name in by_mushaf if name not in EXPECTED_MUSHAFS)

    if missing or extras:
        raise ValueError(
            "Dataset set mismatch; "
            f"missing={missing or 'None'}, extras={extras or 'None'}"
        )

    return [by_mushaf[name] for name in EXPECTED_MUSHAFS]


def _discover_metrics(headers: List[str]) -> List[Tuple[int, str, str]]:
    non_metric = {"sura", "verse", "text"}
    return [
        (i, headers[i], _short_metric_name(headers[i]))
        for i in range(len(headers))
        if headers[i] and headers[i].lower() not in non_metric
    ]


def _resolve_worksheet(wb, file_path: Path):
    candidates = []
    for sheet_name in wb.sheetnames:
        ws = wb[sheet_name]
        try:
            header_cells = next(ws.iter_rows(min_row=1, max_row=1, values_only=True))
        except StopIteration:
            continue

        headers = [str(h).strip() if h is not None else "" for h in header_cells]
        header_to_idx = {h.lower(): i for i, h in enumerate(headers)}
        if "sura" not in header_to_idx or "verse" not in header_to_idx:
            continue

        discovered = _discover_metrics(headers)
        if len(discovered) != 7:
            continue

        short_names = {_short_metric_name(name) for _, name, _ in discovered}
        if short_names != set(METRIC_ORDER):
            continue

        candidates.append(ws)

    if len(candidates) != 1:
        raise ValueError(
            f"Expected exactly one valid worksheet in {file_path.name}; found {len(candidates)}"
        )

    return candidates[0]


def _digit_length(value: int) -> int:
    """Return number of decimal digits for an integer value."""
    return len(str(abs(value)))


def analyze_file(
    file_path: Path,
) -> Tuple[int, Dict[str, int], List[str], List[Dict[str, object]], List[str]]:
    mushaf_name = _mushaf_name_from_file(file_path)
    wb = load_workbook(file_path, read_only=True, data_only=True)
    verses_scanned = 0
    metric_hits: Dict[str, int] = defaultdict(int)
    detail_lines: List[str] = []
    verse_records: List[Dict[str, object]] = []
    try:
        ws = _resolve_worksheet(wb, file_path)

        header_cells = next(ws.iter_rows(min_row=1, max_row=1, values_only=True))
        headers = [str(h).strip() if h is not None else "" for h in header_cells]
        header_to_idx = {h.lower(): i for i, h in enumerate(headers)}

        if "sura" not in header_to_idx or "verse" not in header_to_idx:
            raise ValueError("Missing required columns: sura and verse")

        sura_idx = header_to_idx["sura"]
        verse_idx = header_to_idx["verse"]
        if "text" not in header_to_idx:
            raise ValueError("Missing required column: text")
        text_idx = header_to_idx["text"]

        discovered_metrics: List[Tuple[int, str, str]] = _discover_metrics(headers)
        if len(discovered_metrics) != 7:
            raise ValueError(f"Expected 7 metrics, found {len(discovered_metrics)} in {file_path.name}")

        metric_by_short = {short: (idx, full, short) for idx, full, short in discovered_metrics}
        missing = [m for m in METRIC_ORDER if m not in metric_by_short]
        extras = [m for m in metric_by_short if m not in METRIC_ORDER]
        if missing or extras:
            raise ValueError(
                f"Metric headers mismatch in {file_path.name}; missing={missing or 'None'}, extras={extras or 'None'}"
            )

        metric_columns: List[Tuple[int, str, str]] = [metric_by_short[m] for m in METRIC_ORDER]

        for row_number, row in enumerate(ws.iter_rows(min_row=2, values_only=True), start=2):
            if row is None:
                continue

            sura = row[sura_idx] if sura_idx < len(row) else None
            verse = row[verse_idx] if verse_idx < len(row) else None

            if sura is None or verse is None:
                raise ValueError(f"Missing sura/verse at {file_path.name} row {row_number}")

            sura_num = _to_int(sura)
            verse_num = _to_int(verse)
            if sura_num is None or verse_num is None:
                raise ValueError(f"Invalid sura/verse at {file_path.name} row {row_number}; both must be integers")

            if not 1 <= sura_num <= 114:
                raise ValueError(f"Invalid sura at {file_path.name} row {row_number}; got {sura_num}")

            verses_scanned += 1
            verse_hits: List[str] = []
            metric_records: Dict[str, Dict[str, object]] = {}

            row_text = row[text_idx] if text_idx < len(row) else None
            m4_digits, m6_digits = m4_m6_abjad_digits(row_text)

            for col_idx, metric_name, metric_key in metric_columns:
                if metric_key == "M4":
                    metric_value = int(m4_digits)
                elif metric_key == "M6":
                    metric_value = int(m6_digits)
                else:
                    if col_idx >= len(row):
                        raise ValueError(
                            f"Missing metric cell for {metric_key} at {file_path.name} row {row_number}"
                        )
                    metric_value = _to_int(row[col_idx])
                    if metric_value is None:
                        raise ValueError(
                            f"Invalid metric value for {metric_key} at {file_path.name} row {row_number}; must be integer"
                        )

                mod_19 = metric_value % 19
                is_multiple = mod_19 == 0
                metric_records[metric_key] = {
                    "number": str(metric_value),
                    "digit_length": _digit_length(metric_value),
                    "mod19": mod_19,
                    "is_multiple_of_19": is_multiple,
                }

                if is_multiple:
                    metric_hits[metric_key] += 1
                    verse_hits.append(f"{metric_key}={metric_value}")

            verse_records.append(
                {
                    "sura": sura_num,
                    "verse": verse_num,
                    "metrics": metric_records,
                }
            )

            if verse_hits:
                detail_lines.append(
                    f"  - {mushaf_name} | {sura_num}:{verse_num} -> " + ", ".join(verse_hits)
                )
    finally:
        wb.close()
    return verses_scanned, metric_hits, detail_lines, verse_records


def build_summary(input_dir: Path, output_file: Path) -> None:
    excel_files = _collect_excel_files(input_dir)

    lines: List[str] = []
    bool_lines: List[str] = []
    generated_at = _generated_timestamp()
    lines.append("VERSE LOCK DATASET SUMMARIES")
    lines.append(f"Generated: {generated_at}")
    lines.append("")
    lines.append(f"Datasets scanned: {len(excel_files)}")
    lines.append("")
    bool_lines.append("VERSE LOCK DATASET SUMMARIES (BOOLEAN)")
    bool_lines.append(f"Generated: {generated_at}")
    bool_lines.append("")
    bool_lines.append(f"Datasets scanned: {len(excel_files)}")
    bool_lines.append("")

    json_payload: Dict[str, object] = {
        "schema_version": "1.2",
        "summary_type": "verse_lock",
        "generated": generated_at,
        "modulus": 19,
        "metric_order": METRIC_ORDER,
        "datasets_scanned": len(excel_files),
        "datasets": [],
    }

    for index, file_path in enumerate(excel_files, start=1):
        verses_scanned, metric_hits, detail_lines, verse_records = analyze_file(file_path)
        dataset_total_locks = sum(metric_hits.values())
        mushaf_name = _mushaf_name_from_file(file_path)
        short_hits = {k: metric_hits[k] for k in metric_hits}
        normalized_lock_counts = {
            metric: short_hits.get(metric, 0)
            for metric in METRIC_ORDER
        }
        normalized_lock_bools = {metric: count > 0 for metric, count in normalized_lock_counts.items()}

        lines.append(f"DATASET {index}: {mushaf_name}")
        lines.append(f"Verses scanned: {verses_scanned}")
        lines.append(f"Total metric locks (multiples of 19): {dataset_total_locks}")
        lines.append("Locks by metric:")
        bool_lines.append(f"DATASET {index}: {mushaf_name}")
        bool_lines.append(f"Verses scanned: {verses_scanned}")
        bool_lines.append(f"Total metric locks (multiples of 19): {dataset_total_locks > 0}")
        bool_lines.append("Locks by metric:")

        if metric_hits:
            for metric_name in sorted(metric_hits):
                lines.append(f"- {metric_name}: {metric_hits[metric_name]}")
                bool_lines.append(f"- {metric_name}: {metric_hits[metric_name] > 0}")
        else:
            lines.append("- None")
            bool_lines.append("- None")

        lines.append("Verse-level lock details:")
        bool_lines.append("Verse-level lock details:")
        if detail_lines:
            lines.extend(detail_lines)
            for dl in detail_lines:
                bool_lines.append(re.sub(r"=\d+", "=true", dl))
        else:
            lines.append("- None")
            bool_lines.append("- None")

        json_payload["datasets"].append(
            {
                "dataset_index": index,
                "mushaf": mushaf_name,
                "verses_scanned": verses_scanned,
                "total_metric_locks": dataset_total_locks,
                "total_metric_locks_bool": dataset_total_locks > 0,
                "locks_by_metric": normalized_lock_counts,
                "locks_by_metric_bool": normalized_lock_bools,
                "verse_records": verse_records,
            }
        )

        if index != len(excel_files):
            lines.append("")
            bool_lines.append("")

    output_file.parent.mkdir(parents=True, exist_ok=True)
    output_file.write_text("\n".join(lines) + "\n", encoding="utf-8")
    bool_output = output_file.parent / (output_file.stem + "_bool.txt")
    bool_output.write_text("\n".join(bool_lines) + "\n", encoding="utf-8")
    json_dir = output_file.parent.parent / "json"
    json_dir.mkdir(parents=True, exist_ok=True)
    with (json_dir / (output_file.stem + ".json")).open("w", encoding="utf-8") as handle:
        json.dump(json_payload, handle, ensure_ascii=True, indent=2)


def main() -> None:
    verify_code = hash_registry.cmd_verify_signed()
    if verify_code != 0:
        raise SystemExit(
            "Aborting: signed integrity verification failed. "
            "Run 'python hash_registry.py --verify-signed' for details."
        )

    root = Path(__file__).resolve().parent
    output_path = root / "summary" / "txt" / "grand_verse_lock_summary.txt"
    build_summary(root, output_path)
    print(f"Summary written to: {output_path}")


if __name__ == "__main__":
    main()
