"""
TrustChain — Federation Module
The spoke-and-wheel trust network.

    ┌─────────────────────────────────────────────────┐
    │            MARKET2AGENT HUB  (the wheel)        │
    │                                                 │
    │  Canonical Score · 9 Sensors · SHA-256 Chain    │
    │                                                 │
    │  OBSERVES spoke behavior. NEVER accepts spoke   │
    │  data into canonical scoring. Spoke usage       │
    │  patterns become trust signals ABOUT the spoke. │
    │                                                 │
    │  Bob's trust goes up because we WATCH him use   │
    │  TrustChain honestly — not because he told us   │
    │  he's honest.                                   │
    └──────────┬──────────┬──────────┬────────────────┘
               │          │          │
        ┌──────┴──┐ ┌─────┴───┐ ┌───┴──────┐
        │ Bob     │ │ Alice   │ │ Carlos   │
        │ India   │ │ Berlin  │ │ São Paulo│
        │         │ │         │ │          │
        │ READS   │ │ READS   │ │ READS    │
        │ canon   │ │ canon   │ │ canon    │
        │ score   │ │ score   │ │ score    │
        │         │ │         │ │          │
        │ ADDS    │ │ ADDS    │ │ ADDS     │
        │ local   │ │ local   │ │ local    │
        │ sensors │ │ sensors │ │ sensors  │
        │         │ │         │ │          │
        │ BLENDS  │ │ BLENDS  │ │ BLENDS   │
        │ locally │ │ locally │ │ locally  │
        └─────────┘ └─────────┘ └──────────┘
           spoke        spoke       spoke

SECURITY MODEL (Cloudflare-grade):

    The canonical chain is READ-ONLY to spokes.
    Bob cannot inject, alter, or influence any score.

    But the HUB observes Bob's BEHAVIOR through his API usage:

    ┌────────────────────────────────────────────────┐
    │  SPOKE TRUST SIGNALS (observed, not reported)  │
    ├────────────────────────────────────────────────┤
    │  volume        — verify() calls per period     │
    │  consistency   — checks before transacting?    │
    │  diversity     — varied entities or gaming?    │
    │  anomaly_rate  — pattern looks human or bot?   │
    │  longevity     — months active on TrustChain   │
    │  integration   — middleware/decorator/batch?    │
    │  payment       — paid tier, on time?           │
    │  referrals     — other spokes reference Bob?   │
    └────────────────────────────────────────────────┘

    These signals feed into Bob's OWN trust score on the
    canonical chain. Bob gets more trusted by USING
    TrustChain honestly at scale. Not by claiming trust.

    This is the Cloudflare model:
        Cloudflare doesn't trust your headers.
        Cloudflare trusts your traffic patterns.

    Market2Agent doesn't trust Bob's self-reports.
    Market2Agent trusts Bob's usage patterns.

ANTI-GAMING:

    Bad actor detection uses the same scoring engine
    against the spoke itself:

    - Burst patterns (1000 calls/min) → bot flag
    - Target concentration (same entity 500x) → gaming flag
    - Time-of-day anomalies → automated flag
    - Cross-spoke correlation (Bob and Eve check same
      obscure entities) → collusion flag
    - Score shopping (only checking low-score entities
      then claiming they're trusted) → manipulation flag

    Flagged spokes get their spoke trust REDUCED, which
    means their API key gets rate limited harder, and
    any entity that lists Bob as a reference gets LESS
    boost from that reference.

THE FLYWHEEL:

    Bob installs trustchain →
    Bob verifies entities before transacting →
    Market2Agent observes Bob's honest usage →
    Bob's spoke trust score increases →
    Bob's business gets a higher canonical score →
    Other people verify Bob and see he's trusted →
    They transact with Bob →
    Bob verifies THEM via trustchain →
    More data flows into the canonical chain →
    Better scores for everyone →
    More people install trustchain

    Every spoke makes the wheel stronger.
    Every verify() call makes the chain deeper.
    The data is the moat. The SDK is the distribution.
"""
import os
import time
import hashlib
import logging
from dataclasses import dataclass, field
from typing import Optional, Dict, Any, Callable, List

from trustchain.core import (
    TrustResult,
    TrustClient,
    TrustCheckError,
    verify as canonical_verify,
    verify_async as canonical_verify_async,
    configure,
    _get_key,
    _DEFAULT_API,
)

logger = logging.getLogger("trustchain.federation")


# ═══════════════════════════════════════════════════
# SPOKE RESULT — canonical + local blended
# ═══════════════════════════════════════════════════

@dataclass
class SpokeResult:
    """
    A blended trust result from a spoke node.

    Contains:
        .canonical  — the canonical Market2Agent score (READ ONLY, immutable)
        .local      — the spoke's local score from custom sensors
        .blended    — weighted combination for the spoke's internal use
        .target     — what was scored

    The canonical score is NEVER affected by local sensors.
    The blended score is for the spoke's internal decisions only.
    """
    target: str
    canonical: TrustResult
    local_score: int = 0
    local_sensors: Dict[str, Any] = field(default_factory=dict)
    blend_weight: float = 0.7  # 70% canonical, 30% local by default

    @property
    def blended_score(self) -> int:
        """Weighted blend of canonical + local. For spoke's internal use only."""
        return int(
            self.canonical.score * self.blend_weight
            + self.local_score * (1 - self.blend_weight)
        )

    @property
    def trusted(self) -> bool:
        """Uses canonical trust — local sensors can only ADD caution, not REMOVE it."""
        if not self.canonical.trusted:
            return False
        # Local sensors can flag something the canonical missed
        if self.local_score < 300:
            return False
        return True

    @property
    def canonical_score(self) -> int:
        return self.canonical.score

    @property
    def grade(self) -> str:
        return self.canonical.grade

    def __bool__(self) -> bool:
        return self.trusted

    def __repr__(self) -> str:
        icon = "✓" if self.trusted else "✗"
        return (
            f"SpokeResult({icon} {self.target} "
            f"canon={self.canonical.score} "
            f"local={self.local_score} "
            f"blend={self.blended_score})"
        )


# ═══════════════════════════════════════════════════
# CUSTOM SENSOR — Bob's local trust metrics
# ═══════════════════════════════════════════════════

@dataclass
class SensorRegistration:
    """A custom sensor registered by a spoke."""
    name: str
    fn: Callable
    weight: float = 1.0
    description: str = ""
    max_score: int = 100


# ═══════════════════════════════════════════════════
# TRUST NODE — A spoke in the network
# ═══════════════════════════════════════════════════

class TrustNode:
    """
    A spoke in the TrustChain network.

    Bob creates a TrustNode. He registers custom sensors.
    When he calls node.verify(), he gets canonical + local blended.

    The canonical score is READ-ONLY. Bob's sensors run locally.
    His usage patterns are observed by Market2Agent through
    normal API telemetry (every verify() call is an API call).

        from trustchain.federation import TrustNode

        node = TrustNode(
            name="bob-payments",
            region="ap-south-1",
        )

        @node.sensor("payment_reliability", weight=2.0)
        def check_payments(target):
            rate = get_failure_rate(target)
            return {
                "score": int((1 - rate) * 100),  # 0-100
                "fail_rate": rate,
                "sample_size": 10000,
            }

        @node.sensor("local_latency")
        def check_latency(target):
            ms = ping(target)
            score = 100 if ms < 50 else 80 if ms < 200 else 40
            return {"score": score, "latency_ms": ms}

        result = node.verify("stripe.com")
        result.canonical_score   # 875 (from Market2Agent)
        result.local_score       # 92  (from Bob's sensors)
        result.blended_score     # 640 (70/30 blend)
        result.trusted           # True
    """

    def __init__(
        self,
        name: str,
        region: str = "unknown",
        blend_weight: float = 0.7,
        api_key: Optional[str] = None,
    ):
        """
        Args:
            name: Spoke identifier (e.g. "bob-payments", "acme-vendor-check")
            region: Where this spoke operates (for telemetry, not scoring)
            blend_weight: How much to weight canonical vs local (0.0-1.0)
                         0.7 = 70% canonical, 30% local (default)
                         1.0 = canonical only (local sensors ignored in blend)
                         0.5 = equal weight
            api_key: Market2Agent API key (or set TRUSTCHAIN_API_KEY env var)
        """
        self.name = name
        self.region = region
        self.blend_weight = max(0.0, min(1.0, blend_weight))
        self._sensors: Dict[str, SensorRegistration] = {}
        self._client: Optional[TrustClient] = None

        if api_key:
            configure(api_key=api_key)

        # Spoke identity hash — deterministic, used in telemetry
        self._spoke_id = hashlib.sha256(
            f"{name}:{region}".encode()
        ).hexdigest()[:16]

    def sensor(
        self,
        name: str,
        weight: float = 1.0,
        description: str = "",
        max_score: int = 100,
    ):
        """
        Decorator to register a custom sensor.

            @node.sensor("payment_reliability", weight=2.0)
            def check_payments(target):
                return {
                    "score": 95,  # 0-100 required
                    "fail_rate": 0.002,
                    "sample_size": 10000,
                }

        The function MUST return a dict with at least a "score" key (0-100).
        Additional keys are stored as sensor metadata.

        Args:
            name: Sensor name (unique within this spoke)
            weight: How much this sensor matters in local scoring (default 1.0)
            description: Human-readable description
            max_score: Maximum score this sensor returns (default 100)
        """
        def decorator(fn: Callable) -> Callable:
            self._sensors[name] = SensorRegistration(
                name=name,
                fn=fn,
                weight=weight,
                description=description,
                max_score=max_score,
            )
            return fn
        return decorator

    def add_sensor(
        self,
        name: str,
        fn: Callable,
        weight: float = 1.0,
        description: str = "",
    ):
        """Imperative sensor registration (alternative to decorator)."""
        self._sensors[name] = SensorRegistration(
            name=name, fn=fn, weight=weight, description=description,
        )

    def verify(self, target: str) -> SpokeResult:
        """
        Score an entity with canonical + local sensors.

        1. Calls Market2Agent API for canonical score (READ ONLY)
        2. Runs local sensors
        3. Blends into SpokeResult

        The canonical score is never affected by local sensors.
        Usage telemetry (the fact that this call happened) is
        observed by Market2Agent through normal API metering.
        """
        # Step 1: Canonical score (this is the API call that phones home)
        canonical = canonical_verify(target)

        # Step 2: Run local sensors
        local_score, sensor_results = self._run_local_sensors(target)

        # Step 3: Blend
        return SpokeResult(
            target=target,
            canonical=canonical,
            local_score=local_score,
            local_sensors=sensor_results,
            blend_weight=self.blend_weight,
        )

    async def verify_async(self, target: str) -> SpokeResult:
        """Async version of verify()."""
        canonical = await canonical_verify_async(target)
        local_score, sensor_results = self._run_local_sensors(target)

        return SpokeResult(
            target=target,
            canonical=canonical,
            local_score=local_score,
            local_sensors=sensor_results,
            blend_weight=self.blend_weight,
        )

    def _run_local_sensors(self, target: str) -> tuple:
        """
        Run all registered local sensors. Failures are isolated —
        one bad sensor doesn't break the others.

        Returns (local_score, sensor_results_dict)
        """
        results = {}
        weighted_sum = 0.0
        weight_total = 0.0

        for name, reg in self._sensors.items():
            try:
                output = reg.fn(target)
                if not isinstance(output, dict) or "score" not in output:
                    logger.warning(
                        f"Sensor '{name}' must return dict with 'score' key. Skipping."
                    )
                    results[name] = {"error": "invalid_output", "skipped": True}
                    continue

                score = max(0, min(reg.max_score, int(output["score"])))
                normalized = (score / reg.max_score) * 100

                results[name] = {
                    "score": score,
                    "normalized": normalized,
                    "weight": reg.weight,
                    **{k: v for k, v in output.items() if k != "score"},
                }

                weighted_sum += normalized * reg.weight
                weight_total += reg.weight

            except Exception as e:
                logger.error(f"Sensor '{name}' failed: {e}")
                results[name] = {"error": str(e), "skipped": True}

        # Compute weighted local score (0-1000 scale to match canonical)
        if weight_total > 0:
            local_normalized = weighted_sum / weight_total  # 0-100
            local_score = int(local_normalized * 10)  # 0-1000
        else:
            local_score = 0

        return local_score, results

    @property
    def spoke_id(self) -> str:
        """Deterministic spoke identifier."""
        return self._spoke_id

    @property
    def sensor_count(self) -> int:
        return len(self._sensors)

    def info(self) -> Dict[str, Any]:
        """Spoke metadata."""
        return {
            "spoke_id": self._spoke_id,
            "name": self.name,
            "region": self.region,
            "sensors": list(self._sensors.keys()),
            "sensor_count": len(self._sensors),
            "blend_weight": self.blend_weight,
        }

    def __repr__(self) -> str:
        return f"TrustNode({self.name}@{self.region}, {len(self._sensors)} sensors)"


# ═══════════════════════════════════════════════════
# SPOKE TRUST SCORE — How the hub scores Bob
# ═══════════════════════════════════════════════════
#
# This runs on the HUB (Market2Agent server), not in the SDK.
# Included here as documentation of the scoring model.
#
# Bob's spoke trust is computed from OBSERVED BEHAVIOR:
#
#   SIGNAL                  WEIGHT    HOW OBSERVED
#   ─────────────────────────────────────────────────
#   volume                  15%       API call count per period
#   consistency             20%       Calls before transactions (needs app context)
#   diversity               15%       Unique targets / total calls ratio
#   anomaly_rate            20%       Burst detection, time-of-day patterns
#   longevity               15%       Account age × active months
#   payment_reliability     10%       Stripe payment history
#   referral_quality         5%       Other trusted spokes reference this one
#
# ANTI-GAMING FLAGS (any flag caps spoke trust at 400):
#
#   burst_pattern       — >100 calls/min sustained
#   target_concentration — >50% of calls to same entity
#   time_anomaly        — 95%+ calls in single hour window
#   collusion_signal    — correlated unusual targets with another spoke
#   score_shopping      — only querying known-low entities then claiming trust
#
# A spoke with good behavior for 6+ months, paid tier, diverse targets,
# consistent patterns, and no flags will naturally score 700+.
# That score appears on the CANONICAL chain when someone verify()'s Bob.
#
# Bob doesn't need to do anything special. He just uses TrustChain
# honestly and his score rises. The incentive structure rewards
# legitimate use and punishes gaming.
