"""output_wm.py — Pure function: AFS simulation output → world model.

Every simulation output IS a new world model (Spec 24 P1, Output-World Model Principle).
This module formalizes the conversion:

  strategies (colony run output)
      → factors  (union of concept_weight keys)
      → options  (each strategy; scores = concept_weights × 100)
      → relationships  (Pearson correlation of weights, density-weighted, |r| > 0.3)

Output format: afs_decide input schema (same as IP Core JSON files in src/livenet/cores/).
This format is directly consumable by compile_from_world_model() (via _build_world_model),
afs_compile MCP tool, and Living Network module world model configs.
"""

import numpy as np


def output_to_world_model(strategies, source_label=None, source_run_id=None):
    """Convert AFS colony output (strategies) to a well-formed world model.

    Pure function — no I/O, no side effects. Designed for P2 fire-and-forget
    storage and P3 auto-derivation.

    Args:
        strategies: list of strategy dicts from colony run, each with:
            - label: str
            - cell_count: int  (used for density weighting)
            - concept_weights: dict  (factor_name → float 0.0–1.0)
        source_label: human-readable label for the originating run/question
        source_run_id: optional run ID for provenance tracking

    Returns:
        World model dict in afs_decide input format:
            {
                "question": str,
                "factors": [{"name": str, "range": [0, 100], "unit": "score"}, ...],
                "options": [{"name": str, factor_name: score, ...}, ...],
                "relationships": [{"from": str, "to": str, "strength": float}, ...],
                "_derived": True,
                "_strategy_count": int,
                "_source_run_id": str  (if provided),
            }
        Returns None if strategies is empty.
    """
    if not strategies:
        return None

    # Step 1 — Factors: union of all concept_weight keys, preserving insertion order
    all_factor_names = []
    seen_factors = set()
    for s in strategies:
        for k in s.get("concept_weights", {}):
            if k not in seen_factors:
                seen_factors.add(k)
                all_factor_names.append(k)

    factors = [
        {
            "name": name,
            "range": [0, 100],
            "unit": "score",
            "displayName": name.replace("_", " ").title(),
        }
        for name in all_factor_names
    ]

    # Step 2 — Options: each strategy becomes an option
    # Scores: concept_weights[factor] × 100, zero-fill missing factors
    options = []
    for s in strategies:
        weights = s.get("concept_weights", {})
        opt = {"name": s["label"]}
        for name in all_factor_names:
            opt[name] = round(weights.get(name, 0.0) * 100, 2)
        options.append(opt)

    # Step 3 — Relationships: density-weighted Pearson correlation
    # Emit relationship for each factor pair where |r| > 0.3
    # Strength is signed: positive = co-occurring, negative = anti-correlated (tradeoff)
    relationships = _derive_relationships(strategies, all_factor_names)

    # Step 4 — Assemble
    question = f"Derived from: {source_label}" if source_label else "Derived world model"
    wm = {
        "question": question,
        "factors": factors,
        "options": options,
        "relationships": relationships,
        "_derived": True,
        "_strategy_count": len(strategies),
    }
    if source_run_id is not None:
        wm["_source_run_id"] = str(source_run_id)

    return wm


def _derive_relationships(strategies, factor_names):
    """Compute density-weighted Pearson correlations and return relationship list.

    Only pairs with |r| > 0.3 are included. Upper-triangle only (i < j).
    Signed strength: negative values indicate tradeoff relationships.
    """
    n = len(strategies)
    if n < 2:
        return []

    # Normalised density weights
    cell_counts = [max(s.get("cell_count", 1), 1) for s in strategies]
    total = sum(cell_counts)
    w = np.array([c / total for c in cell_counts], dtype=np.float64)

    # Build per-factor weight vectors (0.0–1.0 space)
    vectors = {}
    for name in factor_names:
        vectors[name] = np.array(
            [s.get("concept_weights", {}).get(name, 0.0) for s in strategies],
            dtype=np.float64,
        )

    relationships = []
    for i, fi in enumerate(factor_names):
        for j, fj in enumerate(factor_names):
            if j <= i:
                continue  # upper triangle only — avoid duplicates
            r = _weighted_pearson(vectors[fi], vectors[fj], w)
            if r is None or abs(r) <= 0.3:
                continue
            # Signed strength: negative = trades_off (anti-correlated)
            signed_strength = round(abs(r) if r > 0 else -abs(r), 4)
            relationships.append({
                "from": fi,
                "to": fj,
                "strength": signed_strength,
            })

    return relationships


def _weighted_pearson(x, y, w):
    """Compute density-weighted Pearson correlation coefficient.

    Args:
        x, y: np.ndarray of float values (same length N)
        w: np.ndarray of normalised weights (sum = 1, length N)

    Returns:
        float in [-1, 1], or None if variance is effectively zero.
    """
    mx = np.sum(w * x)
    my = np.sum(w * y)
    dx = x - mx
    dy = y - my

    cov = np.sum(w * dx * dy)
    var_x = np.sum(w * dx * dx)
    var_y = np.sum(w * dy * dy)

    denom = np.sqrt(var_x * var_y)
    if denom < 1e-10:
        return None

    return float(cov / denom)
