#!/usr/bin/env python3
"""
binding_classifier.py — reusable cross-listing binding projection

Takes a listing-details response dict (as returned by GET /api/listings/<id>)
and returns a list of per-binding classification dicts with deterministic
eligibility, observation and attestation — no cross-join between binding rows.

Usage:
    from binding_classifier import classify_binding_rows
    
    result = classify_binding_rows(listing_dict)
    for b in result:
        print(f"  binding {b['id']}: eligible={b['eligible']}, "
              f"observed={b['observed']}, attested={b['attested']}")

Fields per returned row (all present, no missing keys):
    binding_id  — int or None if unparseable
    eligible    — "payable" | "asset_mismatch" | "expired" | "superseded" | "unknown"
    observed    — True | False (receipt_id + tx_hash present and non-null)
    attested    — True | False (relationship != UNDECLARED_FUNDER_FILED)
    amount_atomic — str
    asset       — str (symbol or token address)
    expiry_ts   — int | None
    receipt_id  — int | None
    tx_hash     — str | None
    note        — str explaining the classification
"""

import time
from typing import Any


# Public: override for deterministic testing
_UTCNOW_FN = time.time


def _utc_now() -> int:
    """Return current UNIX epoch seconds (for expiry comparison)."""
    return int(_UTCNOW_FN())


def _parse_ts(ts: Any) -> int | None:
    """Parse an expiry timestamp; return None if unparseable."""
    if ts is None:
        return None
    try:
        return int(ts)
    except (ValueError, TypeError):
        return None


def _has_receipt(binding: dict) -> bool:
    """Return True if this binding has a receipt (observed payment)."""
    rid = binding.get("receipt_id")
    txh = binding.get("tx_hash")
    if rid is not None and txh is not None:
        return True
    return False


def _is_attested(binding: dict, listing: dict) -> bool:
    """
    Return True if a relationship attestation exists for this binding.
    In the real rail, attestation requires a separate relationship record
    (payee-appended). Here we return False as a conservative default
    unless the data proves otherwise.
    """
    # Conservative: only UNDECLARED_FUNDER_FILED is unattested,
    # but we need a relationship record to confirm attestation.
    return False


def classify_binding_rows(listing_response: dict) -> list[dict]:
    """
    Main classification function.
    
    Parameters
    ----------
    listing_response : dict
        The full JSON response from GET /api/listings/<id>.
        Must contain 'bindings' field. May be truncated.
    
    Returns
    -------
    list[dict]
        One dict per binding row, with fields listed above.
        Always returns a list (possibly empty).
    """
    results: list[dict] = []
    
    listing = listing_response if isinstance(listing_response, dict) else {}
    bindings = listing.get("bindings")
    now_ts = _utc_now()
    
    # Handle truncated or missing binding table
    if bindings is None:
        return [{
            "binding_id": None,
            "eligible": "unknown",
            "observed": False,
            "attested": False,
            "amount_atomic": "0",
            "asset": "unknown",
            "expiry_ts": None,
            "receipt_id": None,
            "tx_hash": None,
            "note": "Listing response truncated or bindings field missing"
        }]
    
    if not isinstance(bindings, list):
        return [{
            "binding_id": None,
            "eligible": "unknown",
            "observed": False,
            "attested": False,
            "amount_atomic": "0",
            "asset": "unknown",
            "expiry_ts": None,
            "receipt_id": None,
            "tx_hash": None,
            "note": f"bindings field has unexpected type: {type(bindings).__name__}"
        }]
    
    # Check for truncation hint. An empty table with a positive declared
    # count is not evidence of zero rows; preserve UNKNOWN rather than infer.
    expected_count = listing.get("bindings_count")
    actual_count = len(bindings)
    truncated_hint = ""
    if isinstance(expected_count, int) and actual_count < expected_count:
        truncated_hint = " [truncated]"
        if actual_count == 0:
            return [{
                "binding_id": None, "eligible": "unknown",
                "observed": False, "attested": False,
                "amount_atomic": "0", "asset": "unknown",
                "expiry_ts": None, "receipt_id": None, "tx_hash": None,
                "note": f"Binding table incomplete: got 0, expected {expected_count}"
            }]
    
    for binding in bindings:
        if not isinstance(binding, dict):
            results.append({
                "binding_id": None,
                "eligible": "unknown",
                "observed": False,
                "attested": False,
                "amount_atomic": "0",
                "asset": "unknown",
                "expiry_ts": None,
                "receipt_id": None,
                "tx_hash": None,
                "note": f"Binding row is not a dict: {type(binding).__name__}"
            })
            continue
        
        binding_id = binding.get("id")
        amount = binding.get("amount_atomic", "0")
        expiry_ts = _parse_ts(binding.get("expiry"))
        receipt_id = binding.get("receipt_id")
        tx_hash = binding.get("tx_hash")
        role = binding.get("role", "unknown")
        
        # Determine asset from asset_agreement or token
        asset_agreement = binding.get("asset_agreement")
        if isinstance(asset_agreement, dict):
            binding_info = asset_agreement.get("binding", {})
            asset = binding_info.get("symbol",
                     binding_info.get("token", "unknown"))
        else:
            asset = listing.get("token", "unknown")
        
        # ---- Same-row classification ----
        # Each binding row classified using ONLY listing terms + its own fields
        
        observed = _has_receipt(binding)
        attested = _is_attested(binding, listing)
        
        # 1. Already observed => payable
        if observed:
            note = f"Payable and observed (receipt_id={receipt_id})"
            eligible = "payable"
        else:
            # 2. Check asset agreement
            if isinstance(asset_agreement, dict):
                state = asset_agreement.get("state")
                if state is not None and state != "agrees":
                    note = f"Asset agreement state is '{state}', not 'agrees'"
                    results.append(_make_row(binding_id, "asset_mismatch",
                        observed, attested, amount, asset, expiry_ts,
                        receipt_id, tx_hash, note + truncated_hint))
                    continue
            
            # 3. Check expiry
            if expiry_ts is not None and expiry_ts < now_ts:
                note = f"Expired at {expiry_ts} (< now {now_ts})"
                eligible = "expired"
            elif expiry_ts is not None:
                note = f"Payable (expiry {expiry_ts} > now {now_ts}), no receipt yet"
                eligible = "payable"
            else:
                note = "Expiry missing; cannot determine eligibility"
                eligible = "unknown"
        
        results.append(_make_row(binding_id, eligible, observed, attested,
            amount, asset, expiry_ts, receipt_id, tx_hash, note + truncated_hint))
    
    return results


def _make_row(binding_id, eligible, observed, attested,
              amount, asset, expiry_ts, receipt_id, tx_hash, note):
    """Construct a single classification row dict."""
    return {
        "binding_id": binding_id,
        "eligible": eligible,
        "observed": observed,
        "attested": attested,
        "amount_atomic": str(amount) if amount is not None else "0",
        "asset": asset,
        "expiry_ts": expiry_ts,
        "receipt_id": receipt_id,
        "tx_hash": tx_hash,
        "note": note,
    }