"""
Sophi Verifier
Checks that the swap program is in equilibrium:
  every party's 'gives' must match exactly one other party's 'wants'.

For plain values: amounts and assets must be numerically equal.
For blind values: the hash on one side must equal the hash on the other.
"""

from typing import List, Tuple
from .parser import SwapProgram, PartyDecl, PlainValue, BlindValue, Side

class VerifyError(Exception):
    pass

# ── Value equality ─────────────────────────────────────────────────────────────

def _values_match(a: object, b: object) -> bool:
    if type(a) != type(b):
        return False
    if isinstance(a, PlainValue):
        return a.asset == b.asset and abs(a.amount - b.amount) < 1e-10
    if isinstance(a, BlindValue):
        return a.hash == b.hash
    return False

def _value_str(v: object) -> str:
    if isinstance(v, PlainValue):
        return f"{v.amount} {v.asset}"
    if isinstance(v, BlindValue):
        return f"blind({v.hash[:8]}...)"
    return str(v)

# ── Equilibrium check ──────────────────────────────────────────────────────────

def verify(program: SwapProgram) -> List[str]:
    """
    Returns a list of human-readable warnings (non-fatal).
    Raises VerifyError on any fatal imbalance.
    """
    parties = program.parties
    warnings = []

    # Build give → want matching
    unmatched_gives = []
    for party in parties:
        unmatched_gives.append((party.name, party.gives.value, party.gives.partial))

    unmatched_wants = []
    for party in parties:
        unmatched_wants.append((party.name, party.wants.value, party.wants.partial))

    matched_gives = set()
    matched_wants = set()

    for gi, (giver, give_val, give_partial) in enumerate(unmatched_gives):
        for wi, (wanter, want_val, want_partial) in enumerate(unmatched_wants):
            if giver == wanter:
                continue  # skip self
            if wi in matched_wants:
                continue
            if _values_match(give_val, want_val):
                matched_gives.add(gi)
                matched_wants.add(wi)
                break

    # Report unmatched gives
    errors = []
    for gi, (giver, give_val, _) in enumerate(unmatched_gives):
        if gi not in matched_gives:
            # Find closest mismatch for helpful error
            candidates = [
                (wanter, want_val)
                for wanter, want_val, _ in unmatched_wants
                if wanter != giver
            ]
            if candidates:
                wanter, want_val = candidates[0]
                errors.append(
                    f"  Balance error: {giver} gives {_value_str(give_val)}, "
                    f"but {wanter} wants {_value_str(want_val)}.\n"
                    f"  These don't match — the balances are not in equilibrium."
                )
            else:
                errors.append(
                    f"  Balance error: {giver} gives {_value_str(give_val)} "
                    f"but nobody wants it."
                )

    if errors:
        msg = "Sophi: swap is not in equilibrium.\n\n" + "\n\n".join(errors)
        raise VerifyError(msg)

    # Warn on mixed blind/plain
    has_blind = any(
        isinstance(p.gives.value, BlindValue) or isinstance(p.wants.value, BlindValue)
        for p in parties
    )
    has_plain = any(
        isinstance(p.gives.value, PlainValue) or isinstance(p.wants.value, PlainValue)
        for p in parties
    )
    if has_blind and has_plain:
        warnings.append(
            "Warning: swap mixes blind and plain values. "
            "Consider using blind() for all sides for full privacy."
        )

    return warnings
