"""Counterexample search at γ ∈ {13, 14, 15}, threshold m ≥ γ.

The earlier search (tire_fiber_counterexample_search.py) capped n ≤ 27
to fit fiber arrays in memory, which forced m_1 < γ at γ ≥ 13 — so it
never tested the threshold-satisfying regime above γ = 12.  This
script uses tire_fiber_chunked.projection_support_streaming to push
n up to 30+ (at γ = 15, m = 15, the bottleneck is ~7 min per tire).

Configs include all-3-faces chord matchings (where they exist —
γ ∈ {15} via the multi-chord 'ring' (0,3),(3,6),...,(γ-3,γ),(0,γ-3))
plus mixed-face chord matchings (γ ∈ {13, 14}).

Shares the same counterexample_search.log with the earlier sweep;
deduplication via pair-keys lets this be resumed mid-flight.
"""
from __future__ import annotations

import json
import time
from pathlib import Path

from tire_fiber_chords import (
    compute_faces_from_chords,
    d_positions_for,
    u_positions_for,
)
from tire_fiber_chunked import projection_support_streaming


HERE = Path(__file__).resolve().parent
LOG_PATH = HERE / "counterexample_search.log"


def log_event(event: dict) -> None:
    event.setdefault('wall_time', time.time())
    with open(LOG_PATH, 'a') as f:
        f.write(json.dumps(event, default=str) + "\n")
        f.flush()


def has_all_three_faces(k: int, chords) -> bool:
    if not chords:
        return k == 3
    faces = compute_faces_from_chords(k, chords)
    return all(len(f) == 3 for f in faces)


def make_key(gamma, m_1, ch1, model1, k_2, ch2, model2) -> str:
    ch1_str = json.dumps(sorted([sorted(c) for c in ch1])) if ch1 else "[]"
    ch2_str = json.dumps(sorted([sorted(c) for c in ch2])) if ch2 else "[]"
    return f"gamma={gamma}|t1=({m_1},{ch1_str},{model1})|t2=({k_2},{ch2_str},{model2})"


def read_existing_keys() -> set[str]:
    keys = set()
    if not LOG_PATH.exists():
        return keys
    with open(LOG_PATH) as f:
        for line in f:
            try:
                ev = json.loads(line)
            except json.JSONDecodeError:
                continue
            if ev.get('type') == 'pair' and 'key' in ev:
                keys.add(ev['key'])
    return keys


# T1 chord configurations at the higher γ.
T1_CONFIGS = {
    13: [
        # Mixed-face (no all-3 possible at γ=13, since 13 not divisible by 3)
        [(0, 3), (4, 7), (8, 11), (12, 1)],
        [(0, 3), (3, 6), (6, 9), (9, 12), (12, 0)],
    ],
    14: [
        # Mixed-face
        [(0, 3), (3, 6), (6, 9), (9, 12), (12, 0)],
        [(0, 3), (4, 7), (8, 11), (12, 0)],
    ],
    15: [
        # all-3 "ring" config: 5 chords, all faces size 3
        [(0, 3), (3, 6), (6, 9), (9, 12), (0, 12)],
        # mixed-face alternative
        [(0, 3), (4, 7), (8, 11), (12, 14), (12, 0)],
    ],
}

# T2 chord configurations (only k_2 ≥ γ to satisfy threshold).
T2_CONFIGS = {
    13: [
        [(0, 3), (4, 7), (8, 11), (12, 1)],
    ],
    14: [
        [(0, 3), (3, 6), (6, 9), (9, 12), (12, 0)],
    ],
    15: [
        [(0, 3), (3, 6), (6, 9), (9, 12), (0, 12)],
        [(0, 3), (4, 7), (8, 11), (12, 14), (12, 0)],
    ],
}


# In-memory caches so that a fixed (m_1, gamma, ch1) doesn't get
# re-streamed across multiple T2 variants.
_pi_d_cache: dict = {}
_pi_u_cache: dict = {}


def _cache_key(args):
    return tuple(args[:-1]) + (tuple(tuple(sorted(c)) for c in args[-1]),)


def pi_D_cached(m_1: int, gamma: int, ch1: list) -> set:
    key = (m_1, gamma, tuple(tuple(sorted(c)) for c in ch1))
    if key not in _pi_d_cache:
        d_pos = d_positions_for(m_1, gamma)
        _pi_d_cache[key] = projection_support_streaming(m_1, gamma, ch1, d_pos)
    return _pi_d_cache[key]


def pi_U_cached(gamma: int, k_2: int, ch2: list) -> set:
    key = (gamma, k_2, tuple(tuple(sorted(c)) for c in ch2))
    if key not in _pi_u_cache:
        u_pos = u_positions_for(gamma, k_2)
        _pi_u_cache[key] = projection_support_streaming(gamma, k_2, ch2, u_pos)
    return _pi_u_cache[key]


def test_pair(gamma: int, m_1: int, ch1, k_2: int, ch2) -> dict:
    t0 = time.time()
    S1 = pi_D_cached(m_1, gamma, ch1)
    S2 = pi_U_cached(gamma, k_2, ch2)
    fwd = S1 & S2
    S2_rev = {s[::-1] for s in S2}
    rev = S1 & S2_rev
    dt = time.time() - t0
    return {
        'type': 'pair',
        'key': make_key(gamma, m_1, ch1, 'SP', k_2, ch2, 'SP'),
        'gamma': gamma,
        't1': {'m_1': m_1, 'chords': ch1, 'model': 'SP'},
        't2': {'k_2': k_2, 'chords': ch2, 'model': 'SP'},
        's1_size': len(S1),
        's2_size': len(S2),
        'fwd_size': len(fwd),
        'rev_size': len(rev),
        'compatible': bool(fwd or rev),
        'time_seconds': round(dt, 3),
        't1_all_three': has_all_three_faces(gamma, ch1),
        't2_all_three': has_all_three_faces(k_2, ch2),
    }


def main():
    already = read_existing_keys()
    log_event({'type': 'session_start', 'session_kind': 'threshold-search-streaming'})
    print(f"[start] {len(already)} pairs already tested", flush=True)

    n_tested = 0
    n_ce = 0
    n_ce_all3 = 0
    t_start = time.time()

    for gamma in [13, 14, 15]:
        cs1_list = T1_CONFIGS.get(gamma, [])
        if not cs1_list:
            continue
        log_event({'type': 'k_start', 'gamma': gamma,
                   'session_kind': 'threshold-search-streaming',
                   'n_t1_configs': len(cs1_list)})

        # We only test the threshold m_1 = γ here (n_1 = 2γ).  Larger m_1
        # would saturate even more easily.
        m_1 = gamma

        for ch1 in cs1_list:
            for k_2 in sorted(T2_CONFIGS.keys()):
                if k_2 < gamma:
                    continue  # threshold: k_2 ≥ γ
                for ch2 in T2_CONFIGS[k_2]:
                    key = make_key(gamma, m_1, ch1, 'SP', k_2, ch2, 'SP')
                    if key in already:
                        continue

                    t0 = time.time()
                    print(f"[{(time.time()-t_start)/60:.1f}m] testing γ={gamma} "
                          f"T1=(m={m_1}, ch={ch1}), T2=(k={k_2}, ch={ch2})",
                          flush=True)
                    try:
                        result = test_pair(gamma, m_1, ch1, k_2, ch2)
                    except Exception as e:
                        log_event({'type': 'exception', 'key': key,
                                   'error': type(e).__name__ + ": " + str(e)})
                        print(f"  exception: {e}", flush=True)
                        continue
                    log_event(result)
                    already.add(key)
                    n_tested += 1
                    if not result['compatible']:
                        n_ce += 1
                        if result.get('t1_all_three') and result.get('t2_all_three'):
                            n_ce_all3 += 1
                            log_event({'type': 'COUNTEREXAMPLE',
                                       'strict_latin': True,
                                       'threshold': True,
                                       'pair': result})
                            print(f"  *** THRESHOLD STRICT-LATIN COUNTEREXAMPLE ***",
                                  flush=True)
                        else:
                            log_event({'type': 'COUNTEREXAMPLE',
                                       'pair': result})
                            print(f"  *** counterexample (not strict-Latin) ***",
                                  flush=True)
                    print(f"  -> |S1|={result['s1_size']}, |S2|={result['s2_size']}, "
                          f"fwd={result['fwd_size']}, rev={result['rev_size']}, "
                          f"compat={result['compatible']} ({result['time_seconds']:.1f}s)",
                          flush=True)
        # Free the γ-specific caches before moving to the next γ
        _pi_d_cache.clear()
        _pi_u_cache.clear()

    elapsed_h = (time.time() - t_start) / 3600
    log_event({'type': 'session_end',
               'session_kind': 'threshold-search-streaming',
               'n_tested': n_tested,
               'n_ce': n_ce,
               'n_ce_strict_latin_threshold': n_ce_all3,
               'elapsed_hours': elapsed_h})
    print(f"\n[done] tested {n_tested}, CE {n_ce}, "
          f"strict-Latin threshold CE {n_ce_all3}, "
          f"elapsed {elapsed_h:.2f} hours", flush=True)


if __name__ == '__main__':
    main()
