#!/usr/bin/env python3
"""
verify_quartet.py  -  One-shot Martini Quartet verifier.

Reconstructs a quartet from (MT, K, delta), screens every member with
gmpy2 (BPSW), then runs PFGW base-2 and base-3 Fermat PRP tests on all
four numbers, parses the logs, and prints a single verdict table.

Quartet geometry (DaveField / Martini Glass Conjecture):
    Twin pair      : MT-1, MT+1                 (twin midpoint MT)
    Martini midpt  : MP = MT * K
    Martini pair   : MP - delta/2, MP + delta/2 (prime gap = delta)

What "PRP" means here: PFGW with a fixed base gives a deterministic,
reproducible Fermat probable-prime result. base-2 AND base-3 both
passing is the customary double-PRP standard for a PrimePages PRP
submission. It is NOT a primality proof -- for that, run Primo (ECPP).

Assumes pfgw64.exe is in the same folder you run this from (override
with --pfgw path\\to\\pfgw64.exe).

Usage:
    py verify_quartet.py --mt-file mt5547.txt --k 652598 --delta 5851414
    py verify_quartet.py --mt "320904..." --k 3545 --delta 297914
    py verify_quartet.py ... --outdir q5547 --pfgw "C:\\tools\\pfgw64.exe"
"""

import argparse
import datetime
import os
import re
import subprocess
import sys

try:
    import gmpy2
    from gmpy2 import mpz, is_prime
except ImportError:
    sys.exit("gmpy2 not found. Install with:  pip install gmpy2 --break-system-packages")

BASES = [2, 3]  # the double-PRP standard

# member key -> human label
LABELS = {
    "PTL_MT_minus_1":  "MT-1  (twin low)",
    "PTH_MT_plus_1":   "MT+1  (twin high)",
    "PPL_MP_minus_d2": "MP-d/2 (Martini low)",
    "PPH_MP_plus_d2":  "MP+d/2 (Martini high)",
}


def load_mt(args):
    if args.mt_file:
        with open(args.mt_file) as f:
            raw = f.read()
    elif args.mt:
        raw = args.mt
    else:
        sys.exit("Provide MT via --mt or --mt-file.")
    cleaned = "".join(ch for ch in raw if ch.isdigit())
    if not cleaned:
        sys.exit("No digits found in the MT input.")
    return mpz(cleaned)


def build_quartet(MT, K, delta):
    if delta % 2 != 0:
        sys.exit(f"delta ({delta}) must be even (a gap between two odd primes).")
    half = delta // 2
    MP = MT * K
    members = {
        "PTL_MT_minus_1":  MT - 1,
        "PTH_MT_plus_1":   MT + 1,
        "PPL_MP_minus_d2": MP - half,
        "PPH_MP_plus_d2":  MP + half,
    }
    gap_ok = (members["PPH_MP_plus_d2"] - members["PPL_MP_minus_d2"]) == delta
    return MP, members, gap_ok


def find_pfgw(explicit):
    """Locate the PFGW executable. Try explicit path, then cwd, then PATH."""
    candidates = []
    if explicit:
        candidates.append(explicit)
    # Windows and *nix names, in current working directory
    for name in ("pfgw64.exe", "pfgw64", "pfgw.exe", "pfgw"):
        candidates.append(os.path.join(os.getcwd(), name))
        candidates.append(name)  # bare name -> search PATH
    for c in candidates:
        if os.path.isfile(c):
            return c
        # bare name: let the OS resolve it via PATH at run time
        if os.sep not in c and (os.altsep is None or os.altsep not in c):
            return c
    return explicit or "pfgw64.exe"


def write_input(member_name, n, outdir):
    path = os.path.join(outdir, f"{member_name}.txt")
    with open(path, "w") as f:
        f.write(str(n) + "\n")
    return path


def run_pfgw(pfgw, base, infile, logfile):
    """
    Run one PFGW PRP test. Returns (verdict, detail) where verdict is
    'PRP', 'COMPOSITE', or 'ERROR'. PFGW also sets a return code, but we
    parse stdout/log text because the wording is what we want to record.
    """
    cmd = [pfgw, f"-b{base}", "-f0", f"-l{logfile}", infile]
    try:
        proc = subprocess.run(
            cmd, capture_output=True, text=True, timeout=3600
        )
    except FileNotFoundError:
        return "ERROR", f"PFGW not found at '{pfgw}'"
    except subprocess.TimeoutExpired:
        return "ERROR", "timed out"

    out = (proc.stdout or "") + (proc.stderr or "")
    # PFGW prints e.g. "... is 2-PRP! (0.17s...)" or "... is composite ..."
    if re.search(rf"is {base}-PRP!", out):
        m = re.search(r"\(([^)]*s[^)]*)\)", out)
        return "PRP", (m.group(1) if m else "")
    if re.search(r"\bis composite\b|is not prime|is 3-PRP.*composite", out, re.I):
        return "COMPOSITE", ""
    if "Error opening file" in out:
        return "ERROR", "PFGW could not open the input file"
    # fall back to return code
    if proc.returncode != 0:
        return "ERROR", f"PFGW exit code {proc.returncode}"
    # last resort: report a trimmed tail of output
    tail = out.strip().splitlines()[-1] if out.strip() else "no output"
    return "ERROR", f"unrecognised PFGW output: {tail[:80]}"


def main():
    ap = argparse.ArgumentParser(description="One-shot Martini Quartet verifier (BPSW + PFGW base 2 & 3).")
    g = ap.add_mutually_exclusive_group()
    g.add_argument("--mt", help="MT as a decimal string")
    g.add_argument("--mt-file", help="path to a text file containing MT")
    ap.add_argument("--k", required=True, type=int, help="multiplier K (MP = MT*K)")
    ap.add_argument("--delta", required=True, type=int, help="Martini prime gap delta")
    ap.add_argument("--outdir", default="q_out", help="directory for input/log files")
    ap.add_argument("--pfgw", default=None, help="path to pfgw64 executable (default: look in current folder / PATH)")
    args = ap.parse_args()

    os.makedirs(args.outdir, exist_ok=True)

    MT = load_mt(args)
    K = mpz(args.k)
    delta = mpz(args.delta)
    MP, members, gap_ok = build_quartet(MT, K, delta)
    pfgw = find_pfgw(args.pfgw)

    stamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    print("=" * 64)
    print(f" Martini Quartet verification   {stamp}")
    print("=" * 64)
    print(f" MT          : {MT.num_digits()} digits")
    print(f" MP = MT*K   : {MP.num_digits()} digits   (K={K}, delta={delta})")
    print(f" gap check   : {'OK' if gap_ok else 'FAILED -- check inputs!'}")
    print(f" PFGW binary : {pfgw}")
    if not gap_ok:
        sys.exit(1)

    # ---- BPSW screen ----
    print("\n BPSW screen (gmpy2, near-certain, not reproducible):")
    bpsw = {}
    for name in LABELS:
        bpsw[name] = bool(is_prime(members[name], 25))
        print(f"   {LABELS[name]:24s} {members[name].num_digits():>5d} d   {'PRP' if bpsw[name] else 'COMPOSITE'}")

    # ---- PFGW base 2 & 3 ----
    print("\n PFGW Fermat PRP tests (reproducible):")
    results = {name: {} for name in LABELS}  # name -> base -> verdict
    for name in LABELS:
        infile = write_input(name, members[name], args.outdir)
        for base in BASES:
            logfile = os.path.join(args.outdir, f"{name}.b{base}.log")
            verdict, detail = run_pfgw(pfgw, base, infile, logfile)
            results[name][base] = verdict
            tag = f"b{base}={verdict}"
            extra = f"  ({detail})" if detail else ""
            print(f"   {LABELS[name]:24s} {tag:14s}{extra}")

    # ---- Final verdict ----
    print("\n" + "-" * 64)
    print(" Summary")
    print("-" * 64)
    header = f" {'member':24s}  BPSW   " + "  ".join(f"b{b}" for b in BASES)
    print(header)
    all_good = True
    for name in LABELS:
        row = f" {LABELS[name]:24s}  {'PRP' if bpsw[name] else 'CMP':5s}  "
        for b in BASES:
            v = results[name][b]
            row += f"  {('PRP' if v=='PRP' else v[:3])}"
            if v != "PRP":
                all_good = False
        print(row)

    print("-" * 64)
    if all_good:
        print(" VERDICT: VALID double-PRP quartet.")
        print(" All four members are base-2 AND base-3 Fermat PRP.")
        print(" Reproducible logs are in: " + os.path.abspath(args.outdir))
        print(" This meets the customary bar for a PrimePages PRP submission.")
        print(" (For a proof rather than PRP, run Primo/ECPP next.)")
    else:
        print(" VERDICT: NOT a clean double-PRP quartet -- see rows above.")
        print(" Check the .log files in: " + os.path.abspath(args.outdir))
        sys.exit(2)


if __name__ == "__main__":
    main()
