"""
afs.bridge.cli — Headless Python engine bridge.

Reads a world-model request from stdin (JSON), runs the full afs_decide pipeline
(compile → calibrate → colony → P9 child sims), and writes strategies JSON to stdout.

Called by afsHeadlessService.js via child_process.spawn:
    python3 -m afs.bridge.cli

stdin:  { question, options, factors, relationships, generations? }
stdout: { strategies, total_cells, factor_names, warnings }
stderr: progress / timing logs (ignored by caller)
"""

import json
import math
import sys
import time

import numpy as np

# Redirect stdout → stderr immediately so that all print() calls from the
# engine (GPU init, timing, progress) go to stderr, not stdout.
# The caller (Node.js) reads only stdout for the final JSON result.
_real_stdout = sys.stdout
sys.stdout = sys.stderr


def main():
    t0 = time.time()

    try:
        data = json.load(sys.stdin)
    except Exception as exc:
        sys.stdout = _real_stdout
        json.dump({"error": f"Failed to parse stdin: {exc}"}, sys.stdout)
        sys.exit(1)

    question = data.get("question", "")
    options = data.get("options", [])
    factors = data.get("factors", [])
    relationships = data.get("relationships", [])
    generations = int(data.get("generations", 0))

    if not question:
        sys.stdout = _real_stdout
        json.dump({"error": "question is required"}, sys.stdout)
        sys.exit(1)
    if len(factors) < 3:
        sys.stdout = _real_stdout
        json.dump({"error": "at least 3 factors required"}, sys.stdout)
        sys.exit(1)

    # Import here so import errors surface as JSON errors on stdout
    try:
        from afs.mcp.server import (
            _build_world_model,
            _run_colony,
            _interpret_clusters,
            _run_child_sims,
            _preflight_check,
            _DEFAULTS,
            _DUMMY_ANALYZER,
        )
        from afs.compiler import FieldCompiler
        from afs.calibrator import HomeostasisCalibrator
        from afs.core import AfsEngine, AfsSimulationConfig
    except ImportError as exc:
        sys.stdout = _real_stdout
        json.dump({"error": f"afs import failed: {exc}"}, sys.stdout)
        sys.exit(1)

    if generations <= 0:
        generations = _DEFAULTS["generations_decide"]

    factor_names = [f["name"] for f in factors]

    # ── 1. Build full world model ─────────────────────────────────
    _log("Building world model: {} factors, {} rels, {} options".format(
        len(factors), len(relationships), len(options)))
    world_model = _build_world_model(question, options, factors, relationships)
    preflight = _preflight_check(world_model)
    if preflight.get("warnings"):
        _log("Preflight warnings: {}".format("; ".join(preflight["warnings"][:2])))

    # ── 2. Compile ─────────────────────────────────────────────────
    compiler = FieldCompiler(analyzer=_DUMMY_ANALYZER)
    compile_result = compiler.compile_from_world_model(world_model)
    field = compile_result["field"]
    _log("Compiled: {}D field".format(field.get_dimension_count()))

    # ── 3. Homeostasis calibration ─────────────────────────────────
    n_dims = field.get_dimension_count()
    calibrator = HomeostasisCalibrator(
        field,
        target_cells=_DEFAULTS["initial_cell_count"],
        target_max_pop=_DEFAULTS["max_population"],
    )
    _log("Calibrating homeostasis...")
    config = calibrator.calibrate(verbose=False)
    cal_score = config._calibration.get("score", {})
    _log("Calibrated: {} (clusters={}, score={:.1f})".format(
        config._calibration.get("label", "?"),
        cal_score.get("cluster_count", "?"),
        cal_score.get("total", 0)))

    # If calibration found 0 clusters during probes, the default physics won't
    # produce convergence. Apply a high-gravity preset known to work for
    # multi-option high-D world models (tested: 2 options, 10D, gravity=15).
    if cal_score.get("cluster_count", 0) == 0:
        _log("Calibration found 0 clusters — switching to high-gravity preset")
        config = AfsSimulationConfig.from_dict({
            "engine": {
                "initial_cell_count": _DEFAULTS["initial_cell_count"],
                "max_population": _DEFAULTS["max_population"],
                "ticks_per_generation": 100,
                "force_scale": 0.4,
                "concept_gravity_strength": 15.0,
                "concept_gravity_range": 5.0 * math.sqrt(n_dims),
                "concept_gravity_sharpness": max(6, n_dims // 2),
                "thermal_noise": 0.08,
                "tradeoff_amplifier": 6.0,
                "spawn_spread": 0.35,
            },
            "metabolism": {
                "concept_absorption_range": 3.5,
                "energy_falloff_exponent": 1.5,
                "base_cost_per_dim": 0.002,
            },
        })
        # High-gravity needs more generations to converge
        if generations < 100:
            generations = 100

    # ── 4. Run parent colony ────────────────────────────────────────
    _log("Running colony: {} cells, {} gens".format(
        config.engine.initial_cell_count, generations))
    engine = AfsEngine(field, config, use_gpu=None)
    raw_results = _run_colony(engine, generations)
    _log("Colony done: {} clusters, {} cells in {:.0f}ms".format(
        len(raw_results["clusters"]), raw_results["final_population"],
        (time.time() - t0) * 1000))

    # ── 4b. Retry if too few clusters ──────────────────────────────
    if len(raw_results["clusters"]) < 3:
        _log("Only {} cluster(s) — retrying with boosted forces".format(
            len(raw_results["clusters"])))
        boosted = AfsSimulationConfig.from_dict({
            "engine": {
                "initial_cell_count": _DEFAULTS["initial_cell_count"],
                "max_population": _DEFAULTS["max_population"],
                "ticks_per_generation": 100,
                "force_scale": 0.4,
                "concept_gravity_strength": 15.0,
                "concept_gravity_range": 5.0 * math.sqrt(n_dims),
                "concept_gravity_sharpness": max(6, n_dims // 2),
                "thermal_noise": 0.08,
                "tradeoff_amplifier": 6.0,
            },
            "metabolism": {
                "concept_absorption_range": 3.5,
                "energy_falloff_exponent": 1.5,
                "base_cost_per_dim": 0.002,
            },
        })
        engine2 = AfsEngine(field, boosted, use_gpu=None)
        retry_results = _run_colony(engine2, generations)
        _log("Retry done: {} clusters".format(len(retry_results["clusters"])))
        if len(retry_results["clusters"]) > len(raw_results["clusters"]):
            raw_results = retry_results

    # ── 5. Interpret clusters → labeled strategies ─────────────────
    strategies = _interpret_clusters(
        raw_results["clusters"],
        world_model["concepts"],
        options,
        factor_names,
    )
    strategies.sort(key=lambda s: s["cell_count"], reverse=True)

    # ── 6. P9 child sims (adaptive depth) ──────────────────────────
    if len(strategies) >= 2:
        child_gens = max(30, generations // 3)
        _log("Running child sims: {} strategies, {} child gens".format(
            len(strategies), child_gens))
        strategies = _run_child_sims(
            field, config, strategies, factor_names,
            world_model["concepts"], child_generations=child_gens,
        )
        _log("Child sims done. Sub-strategies attached.")

    # ── 7 (optional). Inverse colony (Spec 32 P1) ──────────────────
    inverse_data = None
    if data.get("inverse"):
        from afs.core.inverse_colony import run_inverse_colony
        _log("Running inverse colony...")
        inverse_result = run_inverse_colony(
            field, config, generations, fwd_strategies=strategies
        )
        inverse_data = {
            "backward": inverse_result["backward"],
            "delta": inverse_result["delta"],
            "zero_crossing": inverse_result["zero_crossing"],
        }
        _log("Inverse colony done")

    elapsed_ms = int((time.time() - t0) * 1000)
    _log("Bridge total: {}ms".format(elapsed_ms))

    result = {
        "strategies": strategies,
        "total_cells": raw_results["final_population"],
        "factor_names": factor_names,
        "warnings": preflight.get("warnings", []),
        "elapsed_ms": elapsed_ms,
    }
    if inverse_data is not None:
        result["inverse"] = inverse_data
    # Restore real stdout and write the JSON result
    sys.stdout = _real_stdout
    json.dump(result, sys.stdout)


def _log(msg):
    """Write progress to stderr — caller ignores it, but visible in dev logs."""
    print(f"[afs-bridge] {msg}", file=sys.stderr)


if __name__ == "__main__":
    main()
