"""inverse_colony.py — Forward-backward colony execution (Spec 32 P1).

Run the colony forward, flip attractor positions with final cell centroids,
run backward, extract delta. The delta IS new information — structure the
forward run could not see from its starting position.
"""

import numpy as np
from ..core import AfsEngine, FieldDefinition, AfsSimulationConfig
from ..compiler import FieldCompiler, WorldModelTranslator
from ..analysis import detect_clusters, compute_concept_weights


# Minimal analyzer sentinel — compile_from_world_model doesn't use it
_DUMMY_ANALYZER = type("NoOpAnalyzer", (), {})()


def run_inverse_colony(field, config, generations=150, fwd_strategies=None):
    """Forward-backward colony execution. Returns forward strategies,
    backward strategies, and the modulation delta between them.

    Args:
        field: FieldDefinition — compiled field from FieldCompiler
        config: AfsSimulationConfig — calibrated simulation config
        generations: int — generations per direction (default 150)
        fwd_strategies: optional pre-computed forward strategies (skip forward run)

    Returns:
        dict with keys: forward, backward, delta, zero_crossing, generations
    """
    # Phase 1: Forward run (always — we need final cell positions for the backward seed)
    engine_fwd = AfsEngine(field, config)
    engine_fwd.initialize()
    _run_gens(engine_fwd, generations)
    fwd_strategies = _detect_and_interpret(engine_fwd)

    # Guard: no clusters found
    if not fwd_strategies:
        return {
            "forward": [],
            "backward": [],
            "delta": {"per_factor": {}, "per_strategy": [], "density_redistribution": {}},
            "zero_crossing": {"midpoint_weights": {}, "deviation": {}, "stability_ranking": []},
            "generations": generations,
        }

    # Phase 2: Build inverse field — SAME original attractors, but influence
    # matrix is transposed and negated. The cells will be pulled by the same
    # attractor positions through REVERSED causal forces.
    inverse_wm = build_inverse_world_model(fwd_strategies, field)
    compiler = FieldCompiler.__new__(FieldCompiler)
    compiler.translator = WorldModelTranslator()
    compiler.analyzer = _DUMMY_ANALYZER
    compile_result = compiler.compile_from_world_model(inverse_wm)
    inverse_field = compile_result["field"]

    # Phase 3: Backward run — cells START from their forward final positions
    # and are pulled toward the ORIGINAL attractors through REVERSED forces.
    # The journey from output to input reveals the gradient — the WHY.
    engine_bwd = AfsEngine(inverse_field, config)
    engine_bwd.initialize()

    # Overwrite cell positions with forward final positions
    fwd_positions = engine_fwd.get_cell_positions()  # N×D from forward run
    bwd_cells = list(engine_bwd.cells.values())
    n_transfer = min(len(bwd_cells), fwd_positions.shape[0])
    for i in range(n_transfer):
        bwd_cells[i].position = fwd_positions[i].copy()
        bwd_cells[i].velocity = np.zeros_like(bwd_cells[i].velocity)

    _run_gens(engine_bwd, generations)
    bwd_strategies = _detect_and_interpret(engine_bwd)

    # Phase 4: Delta extraction
    delta = extract_modulation_delta(fwd_strategies, bwd_strategies)
    zero_crossing = compute_zero_crossing(fwd_strategies, bwd_strategies)

    return {
        "forward": fwd_strategies,
        "backward": bwd_strategies,
        "delta": delta,
        "zero_crossing": zero_crossing,
        "generations": generations,
    }


def build_inverse_world_model(fwd_strategies, original_field):
    """Build inverse world model from forward colony results.

    Maps forward cluster centroids -> new attractors,
    flips concept_weights (1.0 - w), transpose-negates influence matrix.

    Args:
        fwd_strategies: list of strategy dicts from forward run
        original_field: FieldDefinition used for the forward run

    Returns:
        World model dict compatible with FieldCompiler.compile_from_world_model()
    """
    active_dims = original_field.get_active_dimensions()
    dim_names = [d.name for d in active_dims]
    n_dims = len(dim_names)

    # Build concepts from original dimensions
    concepts = []
    for dim in active_dims:
        concepts.append({
            "name": dim.name,
            "displayName": dim.name.replace("_", " ").title(),
            "description": "",
            "nature": "continuous",
            "controllability": "indirect",
            "dataDictionary": {
                "validRange": list(dim.display_range) if dim.display_range else [0, 100],
                "displayUnit": dim.display_unit or "",
            },
        })

    # Build attractors from ORIGINAL attractor positions (not forward centroids).
    # The cells start at the output positions and get pulled toward the original
    # inputs through reversed forces. This is the actual backward journey.
    original_concepts = list(original_field.concepts.values())
    attractors = []
    for concept in original_concepts:
        position = {}
        for j, name in enumerate(dim_names):
            if j < len(concept.position):
                rng = concepts[j]["dataDictionary"]["validRange"]
                position[name] = rng[0] + concept.position[j] * (rng[1] - rng[0])
        attractors.append({
            "name": f"Inverse_{concept.name}",
            "position": position,
            "strength": 0.7,
        })

    # Build relationships — transpose-negate the influence matrix
    original_matrix = original_field.similarity_matrix
    if original_matrix is not None and original_matrix.size > 0:
        inverse_matrix = -original_matrix.T
    else:
        inverse_matrix = np.zeros((n_dims, n_dims))

    relationships = []
    for i in range(n_dims):
        for j in range(n_dims):
            if i == j or abs(inverse_matrix[i, j]) < 0.01:
                continue
            val = inverse_matrix[i, j]
            relationships.append({
                "fromConcept": dim_names[i],
                "toConcept": dim_names[j],
                "direction": -1 if val < 0 else 1,
                "strength": abs(val),
                "nature": "trades_off" if val < 0 else "enables",
                "mechanism": "inverse of original",
            })

    return {
        "domain": "inverse",
        "problem": {
            "statement": "Inverse colony: backward exploration",
            "objectives": ["Explore inverse attractor landscape"],
        },
        "concepts": concepts,
        "relationships": relationships,
        "tradeoffs": [],
        "attractors": attractors,
        "referencePoints": [],
        "confidence": {"overall": "medium", "weakestLink": "", "dataGaps": []},
    }


def extract_modulation_delta(fwd_strategies, bwd_strategies):
    """Compute per-factor and per-strategy deltas between forward and backward.

    Args:
        fwd_strategies: list of forward strategy dicts
        bwd_strategies: list of backward strategy dicts

    Returns:
        dict with: per_factor (signed weight diffs), per_strategy (L2 distances),
        density_redistribution (cell count changes)
    """
    all_factors = set()
    for s in fwd_strategies + bwd_strategies:
        all_factors.update(s.get("concept_weights", {}).keys())

    fwd_total = sum(s["cell_count"] for s in fwd_strategies) or 1
    bwd_total = sum(s["cell_count"] for s in bwd_strategies) or 1

    # Per-factor delta (density-weighted mean difference)
    per_factor = {}
    for f in sorted(all_factors):
        fwd_mean = sum(
            s.get("concept_weights", {}).get(f, 0.0) * s["cell_count"]
            for s in fwd_strategies
        ) / fwd_total
        bwd_mean = sum(
            s.get("concept_weights", {}).get(f, 0.0) * s["cell_count"]
            for s in bwd_strategies
        ) / bwd_total
        per_factor[f] = round(bwd_mean - fwd_mean, 6)

    # Per-strategy L2 centroid distance (match forward to nearest backward)
    per_strategy = []
    for fs in fwd_strategies:
        fc = np.array(fs["centroid_raw"])
        nearest_dist = float("inf")
        nearest_label = None
        for bs in bwd_strategies:
            bc = np.array(bs["centroid_raw"])
            d = float(np.linalg.norm(fc - bc))
            if d < nearest_dist:
                nearest_dist = d
                nearest_label = bs["label"]
        per_strategy.append({
            "forward_label": fs["label"],
            "backward_label": nearest_label,
            "centroid_distance": round(nearest_dist, 6),
        })

    # Density redistribution
    density_redistribution = {
        "forward_distribution": [
            {"label": s["label"], "cell_count": s["cell_count"],
             "fraction": round(s["cell_count"] / fwd_total, 4)}
            for s in fwd_strategies
        ],
        "backward_distribution": [
            {"label": s["label"], "cell_count": s["cell_count"],
             "fraction": round(s["cell_count"] / bwd_total, 4)}
            for s in bwd_strategies
        ],
    }

    return {
        "per_factor": per_factor,
        "per_strategy": per_strategy,
        "density_redistribution": density_redistribution,
    }


def compute_zero_crossing(fwd_strategies, bwd_strategies):
    """Compute the midpoint between forward and backward factor weights.

    The zero-crossing is where forward and backward forces balance — the
    standing wave node. Factors with small deviation are stable (resilient
    to direction); large deviation = directionally sensitive.

    Returns:
        dict with: midpoint_weights (per factor), deviation (per factor),
        stability_ranking (factors sorted by stability)
    """
    all_factors = set()
    for s in fwd_strategies + bwd_strategies:
        all_factors.update(s.get("concept_weights", {}).keys())

    fwd_total = sum(s["cell_count"] for s in fwd_strategies) or 1
    bwd_total = sum(s["cell_count"] for s in bwd_strategies) or 1

    midpoint_weights = {}
    deviation = {}
    for f in sorted(all_factors):
        fwd_val = sum(
            s.get("concept_weights", {}).get(f, 0.0) * s["cell_count"]
            for s in fwd_strategies
        ) / fwd_total
        bwd_val = sum(
            s.get("concept_weights", {}).get(f, 0.0) * s["cell_count"]
            for s in bwd_strategies
        ) / bwd_total
        midpoint_weights[f] = round((fwd_val + bwd_val) / 2.0, 6)
        deviation[f] = round(abs(bwd_val - fwd_val), 6)

    stability_ranking = sorted(deviation.keys(), key=lambda f: deviation[f])

    return {
        "midpoint_weights": midpoint_weights,
        "deviation": deviation,
        "stability_ranking": stability_ranking,
    }


# ─── Internal helpers ─────────────────────────────────────────────────────────

def _run_gens(engine, generations, ticks_per_gen=100):
    """Run N generations on the engine."""
    engine.total_ticks = generations * ticks_per_gen
    for _ in range(generations):
        engine.run_generation(ticks=ticks_per_gen)


def _detect_and_interpret(engine):
    """Run detect_clusters + compute_concept_weights, return strategies."""
    cell_positions = engine.get_cell_positions()
    concept_positions = engine.get_concept_positions()
    concept_names = engine.get_concept_names()
    n_dims = engine.n_dims

    clusters_raw = detect_clusters(cell_positions)
    strategies = []
    for i, cl in enumerate(clusters_raw):
        weights = compute_concept_weights(cl, concept_positions, concept_names, n_dims)
        strategies.append({
            "label": f"Strategy {i + 1}",
            "cell_count": cl.cell_count,
            "concept_weights": weights,
            "centroid": cl.centroid.tolist(),
            "centroid_raw": cl.centroid.tolist(),
        })
    strategies.sort(key=lambda s: s["cell_count"], reverse=True)
    return strategies
