"""
Perfect Cuboid Search — Core Algorithms
========================================

Implements multiple search strategies for perfect cuboids:
1. Brute-force with modular sieve pre-filtering
2. Parametric search via Pythagorean triple generation
3. Euler brick finder (relaxed: no space diagonal constraint)

Known necessary conditions (Roberts, 2010):
  - One edge odd, two even
  - One edge divisible by 9
  - One edge divisible by 16
  - Space diagonal is odd

Framework axiom mapping:
  - AX2 (Distinction): classify constraint structure
  - AX3 (Selection): modular sieves narrow the space
  - AX4 (Actualization): verify full integer solution
  - AX5 (Integration): all 4 equations must hold simultaneously
  - AX52 (Multiplicative Gate): fail ANY equation → reject
"""

from __future__ import annotations

import math
from dataclasses import dataclass, field
from typing import Generator


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def is_perfect_square(n: int) -> bool:
    """Test if n is a perfect square. O(1) via integer sqrt."""
    if n < 0:
        return False
    r = math.isqrt(n)
    return r * r == n


def integer_sqrt(n: int) -> int | None:
    """Return integer sqrt if n is a perfect square, else None."""
    if n < 0:
        return None
    r = math.isqrt(n)
    return r if r * r == n else None


# ---------------------------------------------------------------------------
# Result type
# ---------------------------------------------------------------------------

@dataclass
class CuboidResult:
    """Result of evaluating a single (a, b, c) triple.

    Attributes:
        a, b, c: edge lengths
        d_ab, d_ac, d_bc: face diagonals (None if not integer)
        d_space: space diagonal (None if not integer)
        is_euler_brick: True if all 3 face diagonals are integer
        is_perfect_cuboid: True if Euler brick AND space diagonal integer
        equations_satisfied: count of satisfied equations (0–4)
    """
    a: int
    b: int
    c: int
    d_ab: int | None = None
    d_ac: int | None = None
    d_bc: int | None = None
    d_space: int | None = None
    is_euler_brick: bool = False
    is_perfect_cuboid: bool = False
    equations_satisfied: int = 0


def check_cuboid(a: int, b: int, c: int) -> CuboidResult:
    """Evaluate a single (a, b, c) triple against all 4 equations.

    This is the AX52 multiplicative gate in action:
    ALL four equations must pass for a perfect cuboid.
    """
    result = CuboidResult(a=a, b=b, c=c)
    satisfied = 0

    # Equation 1: a² + b² = d²_ab
    d_ab = integer_sqrt(a * a + b * b)
    if d_ab is not None:
        result.d_ab = d_ab
        satisfied += 1

    # Equation 2: a² + c² = d²_ac
    d_ac = integer_sqrt(a * a + c * c)
    if d_ac is not None:
        result.d_ac = d_ac
        satisfied += 1

    # Equation 3: b² + c² = d²_bc
    d_bc = integer_sqrt(b * b + c * c)
    if d_bc is not None:
        result.d_bc = d_bc
        satisfied += 1

    # Equation 4: a² + b² + c² = d²_space
    d_space = integer_sqrt(a * a + b * b + c * c)
    if d_space is not None:
        result.d_space = d_space
        satisfied += 1

    result.equations_satisfied = satisfied
    result.is_euler_brick = (satisfied >= 3 and
                             result.d_ab is not None and
                             result.d_ac is not None and
                             result.d_bc is not None)
    result.is_perfect_cuboid = (satisfied == 4)

    return result


# ---------------------------------------------------------------------------
# Modular Sieve — AX3 (Selection)
# ---------------------------------------------------------------------------

def modular_sieve(a: int, b: int, c: int) -> bool:
    """Apply known necessary conditions to quickly reject candidates.

    Known constraints (Roberts 2010, various):
      1. Exactly one edge must be odd, two even
      2. One edge must be divisible by 4
      3. One edge must be divisible by 16
      4. One edge must be divisible by 9
      5. Parity: for a² + b² to be a perfect square,
         a and b cannot both be odd (one of each pair must differ in parity
         or both be even for the sum to admit a square).

    Returns True if the triple PASSES the sieve (could be a cuboid).
    Returns False if the triple is ELIMINATED.
    """
    edges = sorted([a, b, c])

    # Condition 1: Exactly one odd, two even
    odd_count = sum(1 for e in edges if e % 2 == 1)
    if odd_count != 1:
        return False

    # Condition 2: At least one edge divisible by 4
    if not any(e % 4 == 0 for e in edges):
        return False

    # Condition 3: At least one edge divisible by 16
    if not any(e % 16 == 0 for e in edges):
        return False

    # Condition 4: At least one edge divisible by 9
    if not any(e % 9 == 0 for e in edges):
        return False

    return True


# ---------------------------------------------------------------------------
# Pythagorean Triple Generator
# ---------------------------------------------------------------------------

def pythagorean_triples(limit: int) -> Generator[tuple[int, int, int], None, None]:
    """Generate primitive Pythagorean triples (a, b, c) with a < b < c ≤ limit.

    Uses the parametric form:
      a = m² - n², b = 2mn, c = m² + n²
    where m > n > 0, gcd(m,n) = 1, m-n is odd.
    """
    for m in range(2, int(math.sqrt(limit)) + 1):
        for n in range(1, m):
            if (m - n) % 2 == 0:
                continue
            if math.gcd(m, n) != 1:
                continue
            a = m * m - n * n
            b = 2 * m * n
            c = m * m + n * n
            if c > limit:
                break
            if a > b:
                a, b = b, a
            yield (a, b, c)


# ---------------------------------------------------------------------------
# Search Strategies
# ---------------------------------------------------------------------------

def euler_brick_search(limit: int) -> list[CuboidResult]:
    """Search for Euler bricks (3 face diagonals integer) up to given limit.

    Strategy: Generate triples where at least a² + b² is a perfect square,
    then check remaining constraints.

    The smallest known Euler brick: (44, 117, 240).
    """
    results = []
    for a in range(1, limit + 1):
        for b in range(a, limit + 1):
            if not is_perfect_square(a * a + b * b):
                continue
            for c in range(b, limit + 1):
                if not is_perfect_square(a * a + c * c):
                    continue
                if not is_perfect_square(b * b + c * c):
                    continue
                r = check_cuboid(a, b, c)
                if r.is_euler_brick:
                    results.append(r)
    return results


def perfect_cuboid_search(limit: int, use_sieve: bool = True) -> list[CuboidResult]:
    """Exhaustive search for perfect cuboids up to given edge limit.

    Uses modular sieve to prune the search space.
    Returns all Euler bricks found (perfect cuboids would be flagged).

    Args:
        limit: maximum edge length to search
        use_sieve: whether to apply modular sieve pre-filtering

    Returns:
        List of CuboidResult for all Euler bricks (and any perfect cuboids).
    """
    results = []
    checked = 0
    sieved = 0

    for a in range(1, limit + 1):
        for b in range(a, limit + 1):
            for c in range(b, limit + 1):
                checked += 1
                if use_sieve and not modular_sieve(a, b, c):
                    sieved += 1
                    continue
                r = check_cuboid(a, b, c)
                if r.is_euler_brick:
                    results.append(r)

    return results


def parametric_search(max_param: int) -> list[CuboidResult]:
    """Search using parametric Pythagorean triple generation.

    Strategy: Generate pairs of Pythagorean triples that share a leg,
    then check if the third face diagonal and space diagonal are also integer.

    This is more efficient than brute force for finding Euler bricks.
    """
    # Build index: for each leg value, store all triples containing it
    triples_by_leg: dict[int, list[tuple[int, int, int]]] = {}

    for triple in pythagorean_triples(max_param):
        a, b, c = triple
        # Include multiples up to a reasonable bound
        for k in range(1, max_param // c + 1):
            ka, kb, kc = k * a, k * b, k * c
            triples_by_leg.setdefault(ka, []).append((ka, kb, kc))
            triples_by_leg.setdefault(kb, []).append((ka, kb, kc))

    results = []
    seen = set()

    # For each shared leg value, look for compatible triple pairs
    for leg_val, triples_list in triples_by_leg.items():
        for i, t1 in enumerate(triples_list):
            for t2 in triples_list[i + 1:]:
                # t1 and t2 share leg_val
                # Extract the other two legs from each triple
                other1 = [x for x in t1 if x != leg_val]
                other2 = [x for x in t2 if x != leg_val]

                if not other1 or not other2:
                    continue

                # The shared leg is one edge, otherN[0] are hypotenuses or other legs
                # We need to figure out which values are edges vs diagonals
                # For a cuboid edge = leg_val, and the other legs form the remaining edges
                # This is a simplified heuristic — full parametric search is complex
                for o1 in other1:
                    for o2 in other2:
                        edges = tuple(sorted([leg_val, o1, o2]))
                        if edges in seen:
                            continue
                        seen.add(edges)
                        r = check_cuboid(*edges)
                        if r.is_euler_brick:
                            results.append(r)

    return results


# ---------------------------------------------------------------------------
# Near-miss analysis (structural gap measurement — AX34 / M2)
# ---------------------------------------------------------------------------

def find_near_misses(limit: int, max_results: int = 20) -> list[dict]:
    """Find triples that satisfy 3 of 4 equations (near-misses).

    These are Euler bricks — they satisfy all face diagonal constraints
    but fail on the space diagonal.

    Per AX34: The gap (space diagonal not being integer) is a
    "constitutive wound" — a functional interface that reveals
    the structural constraint.

    Per M2: "Measure the wound, don't smooth it." We compute
    how far the space diagonal is from the nearest integer.
    """
    near_misses = []

    for a in range(1, limit + 1):
        for b in range(a, limit + 1):
            if not is_perfect_square(a * a + b * b):
                continue
            for c in range(b, limit + 1):
                r = check_cuboid(a, b, c)
                if r.is_euler_brick and not r.is_perfect_cuboid:
                    # Measure the wound: how far is d_space from integer?
                    s = a * a + b * b + c * c
                    sqrt_s = math.sqrt(s)
                    nearest_int = round(sqrt_s)
                    gap = abs(s - nearest_int * nearest_int)

                    near_misses.append({
                        "edges": (a, b, c),
                        "face_diagonals": (r.d_ab, r.d_ac, r.d_bc),
                        "space_diagonal_squared": s,
                        "space_diagonal_approx": sqrt_s,
                        "nearest_integer_sq": nearest_int * nearest_int,
                        "gap": gap,  # The "constitutive wound" measure
                        "gap_relative": gap / s,
                    })

                    if len(near_misses) >= max_results:
                        return near_misses

    return near_misses
