"""
WorldModelTranslator — Converts world model knowledge into simulation-ready math.

Port of src/afs/worldmodel/WorldModelTranslator.js to Python.

Bridge between Layer 1 (Knowledge) and Layer 3 (Simulation).
Linearizes nonlinear truth tables at the current operating point
and produces a signed N×N matrix that the AfsEngine consumes.
"""

import numpy as np
from typing import Optional


class WorldModelTranslator:
    """
    Converts DomainAnalyzer output (concepts + relationships) into:
    - N×N signed influence matrix
    - Normalized reference point positions
    - Initial cell positions from reference points
    """

    def __init__(self):
        self.initial_dim_count: Optional[int] = None

    # ─── Influence Matrix ──────────────────────────────────────

    def build_influence_matrix(
        self,
        concepts: list[dict],
        relationships: list[dict],
        operating_point: Optional[dict] = None,
    ) -> np.ndarray:
        """
        Build the N×N signed influence matrix from relationship documents.

        Matrix values range from -1.0 to +1.0 where:
          positive = correlated (increasing i increases j)
          negative = trade-off (increasing i decreases j)
          zero = no relationship

        Args:
            concepts: ordered list of concept dicts (must have 'name')
            relationships: relationship dicts with fromConcept, toConcept, direction, strength
            operating_point: optional {concept_name: value} for truth table interpolation

        Returns:
            (N, N) numpy array — signed influence matrix
        """
        n = len(concepts)
        if self.initial_dim_count is None:
            self.initial_dim_count = n
        matrix = np.zeros((n, n), dtype=np.float64)

        # Build name → index lookup
        name_to_idx = {c["name"]: i for i, c in enumerate(concepts)}
        operating_point = operating_point or {}

        for rel in relationships:
            from_concept = rel.get("fromConcept", rel.get("from"))
            to_concept = rel.get("toConcept", rel.get("to"))
            i = name_to_idx.get(from_concept)
            j = name_to_idx.get(to_concept)
            if i is None or j is None:
                continue

            # Evaluate response curve at current operating point
            from_value = operating_point.get(from_concept)
            response_curve = rel.get("responseCurve")
            if from_value is not None and response_curve and response_curve.get("truthTable"):
                local_strength = self.evaluate_response_curve(response_curve, from_value)
                if local_strength is None:
                    local_strength = rel.get("strength", 0.5)
            else:
                local_strength = rel.get("strength", 0.5)

            direction = rel.get("direction", 1)
            matrix[i, j] = direction * local_strength

            # Symmetric reverse if specified
            symmetry = rel.get("symmetry", {})
            if symmetry.get("isSymmetric") and symmetry.get("reverseStrength") is not None:
                base_strength = rel.get("strength", 0.5)
                if from_value is not None and response_curve and response_curve.get("truthTable"):
                    curve_strength = self.evaluate_response_curve(response_curve, from_value)
                    if curve_strength is not None and base_strength > 0:
                        reverse = curve_strength * (symmetry["reverseStrength"] / base_strength)
                    else:
                        reverse = symmetry["reverseStrength"]
                else:
                    reverse = symmetry["reverseStrength"]
                matrix[j, i] = direction * reverse

        return matrix

    # ─── Response Curve Evaluation ─────────────────────────────

    @staticmethod
    def evaluate_response_curve(curve: dict, input_value: float) -> Optional[float]:
        """
        Evaluate a response curve at a specific input value.
        Interpolates the truth table to get effective strength.

        Returns effective strength [0, 1] or None if no table.
        """
        truth_table = curve.get("truthTable", [])
        if not truth_table:
            return None

        # Clamp to table range
        if input_value <= truth_table[0]["fromValue"]:
            return abs(truth_table[0]["effectOnTo"])
        if input_value >= truth_table[-1]["fromValue"]:
            return abs(truth_table[-1]["effectOnTo"])

        # Interpolate
        for k in range(len(truth_table) - 1):
            lower = truth_table[k]
            upper = truth_table[k + 1]
            if lower["fromValue"] <= input_value <= upper["fromValue"]:
                t = (input_value - lower["fromValue"]) / (upper["fromValue"] - lower["fromValue"])
                effect = lower["effectOnTo"] + t * (upper["effectOnTo"] - lower["effectOnTo"])
                return abs(effect)

        return abs(truth_table[0]["effectOnTo"])

    # ─── Re-linearization ──────────────────────────────────────

    def relinearize(
        self,
        cell_positions: np.ndarray,
        concepts: list[dict],
        relationships: list[dict],
    ) -> np.ndarray:
        """
        Re-linearize the influence matrix at the population's current centroid.
        Called periodically (every N generations) to adapt the force landscape.

        Args:
            cell_positions: (C, D) array of cell positions in [0, 1]
            concepts: ordered active concepts
            relationships: relationship documents

        Returns:
            Updated (N, N) influence matrix
        """
        if cell_positions.size == 0:
            return self.build_influence_matrix(concepts, relationships)

        n = len(concepts)
        centroid = {}
        mean_pos = np.mean(cell_positions, axis=0)

        for d in range(n):
            concept = concepts[d]
            dd = concept.get("dataDictionary", {})
            valid_range = dd.get("validRange", [0, 100])
            normalized = float(mean_pos[d]) if d < len(mean_pos) else 0.5
            centroid[concept["name"]] = valid_range[0] + normalized * (valid_range[1] - valid_range[0])

        return self.build_influence_matrix(concepts, relationships, centroid)

    # ─── Reference Point Normalization ─────────────────────────

    @staticmethod
    def normalize_reference_point(ref_point: dict, concepts: list[dict]) -> np.ndarray:
        """
        Normalize reference point positions from real-world units to [0, 1].

        Args:
            ref_point: dict with 'position' mapping concept names to values
            concepts: ordered active concepts

        Returns:
            (N,) numpy array of normalized positions
        """
        n = len(concepts)
        position = np.full(n, 0.5)
        pos_dict = ref_point.get("position", {})

        # Check if already normalized
        vals = [v for v in pos_dict.values() if isinstance(v, (int, float))]
        already_normalized = ref_point.get("normalized", False) or (
            len(vals) > 0 and all(0 <= v <= 1 for v in vals)
        )

        for d, concept in enumerate(concepts):
            raw_value = pos_dict.get(concept["name"])

            if raw_value is None:
                position[d] = 0.5
            elif isinstance(raw_value, str):
                # Hash string to consistent [0.1, 0.9] position
                h = 0
                for ch in raw_value:
                    h = ((h << 5) - h + ord(ch)) & 0xFFFFFFFF
                position[d] = 0.1 + (h % 800) / 1000
            elif already_normalized:
                position[d] = max(0.0, min(1.0, raw_value))
            else:
                dd = concept.get("dataDictionary", {})
                display_range = concept.get("displayRange") or dd.get("validRange", [0, 100])
                lo, hi = display_range[0], display_range[1]
                if hi != lo:
                    position[d] = max(0.0, min(1.0, (raw_value - lo) / (hi - lo)))
                else:
                    position[d] = 0.5

        return position

    # ─── Cell Position Generation ──────────────────────────────

    def create_cell_positions(
        self,
        reference_points: list[dict],
        total_cells: int,
        concepts: list[dict],
        noise: float = 0.05,
    ) -> np.ndarray:
        """
        Create initial cell positions from reference points.
        Each reference point spawns a cluster of cells nearby.

        Args:
            reference_points: list of reference point dicts
            total_cells: total number of cells to create
            concepts: ordered active concepts
            noise: position noise (default 0.05)

        Returns:
            (total_cells, N) numpy array of normalized positions
        """
        n = len(concepts)

        if not reference_points:
            # Fallback: Gaussian around center
            positions = 0.5 + (np.random.random((total_cells, n)) - 0.5) * 0.3
            return np.clip(positions, 0.0, 1.0)

        positions = []
        per_ref = total_cells // len(reference_points)
        remainder = total_cells % len(reference_points)

        for r, rp in enumerate(reference_points):
            base_pos = self.normalize_reference_point(rp, concepts)
            count = per_ref + (1 if r < remainder else 0)

            cluster = np.tile(base_pos, (count, 1))
            cluster += (np.random.random((count, n)) - 0.5) * noise
            positions.append(np.clip(cluster, 0.0, 1.0))

        return np.vstack(positions)

    # ─── Influence Vector ──────────────────────────────────────

    @staticmethod
    def build_influence_vector(concept_index: int, matrix: np.ndarray) -> np.ndarray:
        """
        Build an influence profile vector for one concept.
        Component j = this concept's effect on concept j.
        """
        return matrix[concept_index].copy()
