#!/usr/bin/env python3
"""Independently audit supplied ayah tables; stdlib only, no whole-integer parsing.

Run: python3 AllahKelimesi/independent_swap_audit.py
For EVERY eligible pair, reconstruct both w-bearing decimal strings and stream
128-digit chunks modulo 19. Independently cross-check using prefix/suffix
polynomial arithmetic, including swaps with unequal decimal widths.
This verifies the numeric files, not their derivation from an Arabic text.
"""
import argparse
from collections import Counter, defaultdict
from hashlib import sha256
from itertools import combinations
import json
from pathlib import Path
import time

MOD = 19
FORMATS = {
    'g_s_a_an': ('g', 's', 'a', 'an'),
    'g_s_a_an_ah': ('g', 's', 'a', 'an', 'ah'),
    'g_s_a_an_w': ('g', 's', 'a', 'an', 'w'),
    'g_s_a_an_ah_w': ('g', 's', 'a', 'an', 'ah', 'w'),
}
CANONICAL_COUNTS = [7,286,200,176,120,165,206,75,129,109,123,111,43,52,99,128,
    111,110,98,135,112,78,118,64,77,227,93,88,69,60,34,30,73,54,45,83,182,
    88,75,85,54,53,89,59,37,35,38,29,18,45,60,49,62,55,78,96,29,22,24,
    13,14,11,11,18,12,12,30,52,52,44,28,28,20,56,40,31,50,40,46,42,29,
    19,36,25,22,17,19,26,30,20,15,21,11,8,8,19,5,8,8,11,11,8,3,9,5,4,
    7,3,6,3,5,4,5,6]


def read_table(path, fields):
    rows = []
    for line_no, line in enumerate(path.read_text(encoding='utf-8-sig').splitlines(), 1):
        if not line.strip():
            continue
        tokens = line.split()
        if len(tokens) != len(fields) or any(not t.isascii() or not t.isdigit() for t in tokens):
            raise ValueError(f'{path}:{line_no}: expected {len(fields)} decimal fields')
        row = dict(zip(fields, map(int, tokens)))
        if any(v <= 0 for v in row.values()):
            raise ValueError(f'{path}:{line_no}: nonpositive field')
        rows.append(row)
    return rows


def stream_mod(text):
    # Every conversion is bounded to 128 digits, never the full long integer.
    residue = 0
    for start in range(0, len(text), 128):
        chunk = text[start:start + 128]
        residue = (residue * pow(10, len(chunk), MOD) + int(chunk)) % MOD
    return residue


def digit_mod(text):
    residue = 0
    for char in text:
        residue = (10 * residue + ord(char) - 48) % MOD
    return residue


def make_record(row, fields, override_w=None):
    return ''.join(str(override_w if k == 'w' and override_w is not None else row[k]) for k in fields)


class IndependentSegments:
    """Second algorithm: concatenate unchanged segments and replacement records."""
    def __init__(self, text, records):
        self.prefix = [0]
        self.powers = [1]
        self.offsets = [0]
        for char in text:
            self.prefix.append((10 * self.prefix[-1] + ord(char) - 48) % MOD)
            self.powers.append(10 * self.powers[-1] % MOD)
        for record in records:
            self.offsets.append(self.offsets[-1] + len(record))
        self.length = len(text)

    def segment(self, start, end):
        length = end - start
        return (self.prefix[end] - self.prefix[start] * self.powers[length]) % MOD, length

    def swapped(self, i, j, replacement_i, replacement_j):
        pieces = [self.segment(0, self.offsets[i]),
                  (digit_mod(replacement_i), len(replacement_i)),
                  self.segment(self.offsets[i + 1], self.offsets[j]),
                  (digit_mod(replacement_j), len(replacement_j)),
                  self.segment(self.offsets[j + 1], self.length)]
        residue, length = 0, 0
        for value, size in pieces:
            residue = (residue * pow(10, size, MOD) + value) % MOD
            length += size
        return residue, length


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--root', type=Path, default=Path(__file__).resolve().parent.parent)
    parser.add_argument('--output', type=Path)
    args = parser.parse_args()
    started = time.perf_counter()
    root = args.root.resolve()
    source_path = root / 'kuran.txt'
    allah_path = root / 'AllahKelimesi/17138.txt'
    report_path = root / 'AllahKelimesi/ayet_swap_185_205_dual_all.txt'
    source = read_table(source_path, ('g', 's', 'a', 'w', 'h'))
    selected = read_table(allah_path, ('g', 's', 'a', 'an', 'ah'))
    assert len({r['g'] for r in source}) == len(source)
    assert len({(r['s'], r['a']) for r in source}) == len(source)
    assert len({r['g'] for r in selected}) == len(selected)
    assert len({(r['s'], r['a']) for r in selected}) == len(selected)
    assert [r['g'] for r in source] == list(range(1, len(source) + 1))
    assert [r['g'] for r in selected] == sorted(r['g'] for r in selected)
    by_g = {r['g']: r for r in source}
    rows = []
    for item in sorted(selected, key=lambda r: r['g']):
        original = by_g[item['g']]
        assert (item['s'], item['a']) == (original['s'], original['a'])
        rows.append(original | item)

    observed = {(r['s'], r['a']) for r in source}
    canonical = {(s, a) for s, count in enumerate(CANONICAL_COUNTS, 1) for a in range(1, count + 1)}
    surah_counts = Counter(r['s'] for r in source)
    starts = {}
    for r in source:
        starts.setdefault(r['s'], {'g': r['g'], 'a': r['a']})
    baseline = {}
    records = {}
    independent = {}
    for name, fields in FORMATS.items():
        records[name] = [make_record(row, fields) for row in rows]
        text = ''.join(records[name])
        residue = stream_mod(text)
        assert residue == digit_mod(text)
        baseline[name] = {'length': len(text), 'mod19': residue}
        if 'w' in fields:
            independent[name] = IndependentSegments(text, records[name])

    groups = defaultdict(list)
    for i, row in enumerate(rows):
        groups[row['s'], row['an'], row['ah']].append(i)
    eligible = 0
    unequal_width = 0
    successes = []
    candidate = None
    residue_counts = Counter()
    w_names = list(independent)
    for indices in groups.values():
        for i, j in combinations(indices, 2):
            left, right = rows[i], rows[j]
            if left['w'] == right['w']:
                continue
            eligible += 1
            unequal_width += len(str(left['w'])) != len(str(right['w']))
            mods = []
            results = {}
            for name in w_names:
                fields = FORMATS[name]
                record_list = records[name]
                old_i, old_j = record_list[i], record_list[j]
                new_i = make_record(left, fields, right['w'])
                new_j = make_record(right, fields, left['w'])
                record_list[i], record_list[j] = new_i, new_j
                full_text = ''.join(record_list)
                residue = stream_mod(full_text)
                assert (residue, len(full_text)) == independent[name].swapped(i, j, new_i, new_j)
                assert len(full_text) == baseline[name]['length']
                if residue == 0:
                    assert digit_mod(full_text) == residue
                record_list[i], record_list[j] = old_i, old_j
                results[name] = {'length': len(full_text), 'mod19': residue}
                mods.append(residue)
            residue_counts[tuple(mods)] += 1
            pair = {'s': left['s'], 'a1': left['a'], 'a2': right['a'],
                    'g1': left['g'], 'g2': right['g'], 'w1': left['w'], 'w2': right['w'],
                    'h1': left['h'], 'h2': right['h'], 'an': left['an'], 'ah': left['ah'],
                    'd': right['a'] - left['a']}
            if mods == [0, 0]:
                successes.append(pair)
            if (left['s'], left['a'], right['a']) == (6, 39, 116):
                # an and ah equal, positional g/s/a fixed: first two forms invariant.
                candidate = {'pair': pair, 'after_swap': baseline | results}

    lines = report_path.read_text(encoding='utf-8-sig').splitlines()
    header = lines[0].split()
    expected_header = ['s','a1','a2','g1','g2','w1','w2','h1','h2','an','ah','d']
    assert header == expected_header
    reported = []
    for line in lines[1:]:
        if line.strip():
            values = list(map(int, line.split()))
            assert len(values) == len(header)
            reported.append(dict(zip(header, values)))
    key = lambda r: tuple(r[k] for k in header)
    actual_set, report_set = {key(r) for r in successes}, {key(r) for r in reported}
    as_dicts = lambda tuples: [dict(zip(header, t)) for t in sorted(tuples)]
    comparison = {'reported_rows': len(reported), 'reported_unique_rows': len(report_set),
                  'exact_set_match_all_12_columns': actual_set == report_set,
                  'missing_from_report': as_dicts(actual_set - report_set),
                  'extra_or_incorrect_report_rows': as_dicts(report_set - actual_set)}
    results = {
        'inputs': {str(p.relative_to(root)): {'sha256': sha256(p.read_bytes()).hexdigest()}
                   for p in (source_path, allah_path, report_path)},
        'source_rows': len(source), 'selected_rows': len(rows),
        'input_numbering': {'g_is_contiguous_one_based_source_row_number': True,
                            'selected_g_s_a_all_match_source': True,
                            'comparison_standard': 'Common 6236-ayah / 114-surah numbering',
                            'standard_verse_count': len(canonical),
                            'missing_standard_references': sorted(canonical - observed),
                            'extra_references': sorted(observed - canonical),
                            'surah_count_differences': {str(s): {'actual': surah_counts[s], 'standard': n}
                                for s, n in enumerate(CANONICAL_COUNTS, 1) if surah_counts[s] != n},
                            'surah_starts': starts},
        'baseline': baseline, 'eligible_pairs': eligible,
        'eligible_pairs_with_unequal_w_digit_widths': unequal_width,
        'successful_pairs': len(successes), 'candidate_6_39_116': candidate,
        'report_comparison': comparison,
        'verification': {'all_eligible_pairs_fully_reconstructed_and_streamed': True,
                         'all_eligible_pairs_independently_segment_checked': True,
                         'whole_long_integer_conversion': False,
                         'max_decimal_parse_digits': 128,
                         'success_condition': 'Both w-bearing residues zero; first two invariant',
                         'scope': 'Numeric input consistency only; an/ah/w/h not checked against Arabic text'},
        'successes': sorted(successes, key=lambda r: (r['d'], r['s'], r['a1'], r['a2'])),
        'residue_pair_counts': {f'{a},{b}': n for (a, b), n in sorted(residue_counts.items())},
        'elapsed_seconds': round(time.perf_counter() - started, 3),
    }
    output = args.output or root / 'AllahKelimesi/independent_swap_audit.json'
    output.write_text(json.dumps(results, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
    concise = {k: results[k] for k in ('source_rows', 'selected_rows', 'baseline', 'eligible_pairs',
               'eligible_pairs_with_unequal_w_digit_widths', 'successful_pairs',
               'candidate_6_39_116', 'report_comparison', 'elapsed_seconds')}
    concise['missing_standard_references'] = results['input_numbering']['missing_standard_references']
    concise['output'] = str(output)
    print(json.dumps(concise, ensure_ascii=False, indent=2))


if __name__ == '__main__':
    main()
