# CelerisLab/tests/postproc/run_exp_ctrl_matrix_streakline.py
"""Streakline post-processing for exp_ctrl_matrix cases.

Runs full CFD, uses Streakline.observe() in the last STREAK_WINDOW steps,
renders streakline at the final step.
"""

from __future__ import annotations

import argparse
import json
from pathlib import Path

import numpy as np

_REPO = Path(__file__).resolve().parents[2]

import sys

sys.path.insert(0, str(_REPO / "src"))
sys.path.insert(0, str(_REPO / "tests" / "postproc"))

import run_exp_ctrl_matrix_vorticity as vort
from CelerisLab import Simulation
from CelerisLab.common.streakline import Streakline, ReleaseConfig, IntegratorConfig

DIAMETER_CELLS = vort.DIAMETER_CELLS
DEFAULT_OUT = _REPO / "tests" / "output" / "exp_ctrl_matrix_streak_ny300"
STREAK_WINDOW_STEPS = 20_000
STREAK_SAMPLE_EVERY = 50
STREAK_AGE_DECAY = 100_000.0
STREAK_BLUR_SIGMA = 0.8


def build_release_points_for_triangle(layout: dict) -> np.ndarray:
    release_x = min(layout["x_apex"], layout["x_rear"]) - 4.0 * DIAMETER_CELLS
    y_low_edge = layout["y_lower"] - layout["radius_lb"]
    y_high_edge = layout["y_upper"] + layout["radius_lb"]
    ys = np.linspace(y_low_edge, y_high_edge, 4, dtype=np.float64)
    return np.column_stack([np.full(4, release_x, dtype=np.float64), ys])


def _apply_body_actions(
    sim: Simulation, a1: float, a2: float, a3: float, u_lb: float
) -> None:
    w1 = vort._action_to_omega_lb(a1, u_lb)
    w2 = vort._action_to_omega_lb(a2, u_lb)
    w3 = vort._action_to_omega_lb(a3, u_lb)
    if vort.SWAP_ACTION23_BODIES:
        vort._set_body_omegas(sim, w1, w3, w2)
    else:
        vort._set_body_omegas(sim, w1, w2, w3)


def _cylinders_from_triangle_layout(layout: dict) -> list[tuple[tuple[float, float], float]]:
    radius = float(layout["radius_lb"])
    return [
        ((float(layout["x_apex"]), float(layout["y_center"])), radius),
        ((float(layout["x_rear"]), float(layout["y_lower"])), radius),
        ((float(layout["x_rear"]), float(layout["y_upper"])), radius),
    ]


def run_streak_case(
    case_id: str,
    slug: str,
    features: dict,
    *,
    out_dir: Path,
    total_steps: int,
    streak_window: int,
    sample_every: int,
    report_every: int,
) -> dict:
    streak_start = max(0, int(total_steps) - int(streak_window))
    compat = vort._ensure_compat_config(vort.CONFIG_PATH)
    sim = Simulation(compat)
    layout = vort._add_triangle_cylinders(sim)
    sim.initialize()
    u_lb = float(sim.lbm_cfg.velocity)
    dt_phys = (
        (vort.CYLINDER_DIAMETER_M / DIAMETER_CELLS)
        * (u_lb / vort.INLET_U_PHYS_M_S)
    )
    cylinders = _cylinders_from_triangle_layout(layout)

    release_cfg = ReleaseConfig(
        mode="strip",
        line_count=1,
        line_span=0.0,
        downstream_count=5,
        downstream_spacing=1.0,
        inject_per_seed=1,
    )
    integrator_cfg = IntegratorConfig(
        alpha_t=0.25, alpha_x=0.40, max_particle_age=None
    )
    base_release = build_release_points_for_triangle(layout)

    streak = Streakline(
        release_points=base_release,
        release_cfg=release_cfg,
        integrator_cfg=integrator_cfg,
        nx=int(sim.lbm_cfg.nx),
        ny=int(sim.lbm_cfg.ny),
        cylinders=cylinders,
    )

    print(
        f"--- {case_id} {slug} steps={total_steps} streak_window={streak_window} "
        f"(inject from step {streak_start}) ---"
    )

    for step in range(total_steps):
        t_phys = step * dt_phys
        a1, a2, a3 = vort._actions_at_time(t_phys, features)
        _apply_body_actions(sim, a1, a2, a3, u_lb)
        sim.run(1)

        # Feed streakline within the window
        if step >= streak_start and (step + 1) % sample_every == 0:
            macro = sim.get_macroscopic()
            streak.observe(ux=macro["ux"], uy=macro["uy"], step=int(step + 1))

        if report_every > 0 and (step + 1) % report_every == 0:
            print(
                f"  step {step+1}/{total_steps} a=({a1:+.5f},{a2:+.5f},{a3:+.5f}) "
                f"particles={streak.n_particles}"
            )

    if streak.n_particles == 0:
        raise RuntimeError(
            f"{case_id}: no particles in streak window; lower sample_every."
        )

    png = out_dir / f"streakline_{case_id}_{slug}.png"
    render_info = streak.render(
        str(png),
        age_decay_steps=STREAK_AGE_DECAY,
        blur_sigma=STREAK_BLUR_SIGMA,
        background_color=(1.0, 1.0, 1.0),
        streak_color=(1.0, 0.0, 0.0),
    )
    sim.close()

    summary = {
        "case_id": case_id,
        "slug": slug,
        "total_steps": int(total_steps),
        "streak_window_steps": int(streak_window),
        "streak_start_step": int(streak_start),
        "sample_every": int(sample_every),
        "particle_count_final": int(streak.n_particles),
        "release_points_dense": int(base_release.shape[0]),
        "streak_png": str(png),
        "swap_action23_bodies": bool(vort.SWAP_ACTION23_BODIES),
        "render": render_info,
    }
    with (out_dir / f"summary_{case_id}_{slug}.json").open("w", encoding="utf-8") as f:
        json.dump(summary, f, indent=2)
    print(f"  saved {png} particles={streak.n_particles}")
    return summary


def main() -> int:
    ap = argparse.ArgumentParser(description="exp_ctrl_matrix streakline batch")
    ap.add_argument("--out-dir", type=str, default=str(DEFAULT_OUT))
    ap.add_argument("--steps", type=int, default=vort.FIXED_STEPS)
    ap.add_argument("--streak-window", type=int, default=STREAK_WINDOW_STEPS)
    ap.add_argument("--sample-every", type=int, default=STREAK_SAMPLE_EVERY)
    ap.add_argument("--report-every", type=int, default=20000)
    ap.add_argument("--cases", type=str, default="")
    args = ap.parse_args()

    out_dir = Path(args.out_dir)
    out_dir.mkdir(parents=True, exist_ok=True)
    selected = (
        {c.strip() for c in args.cases.split(",") if c.strip()}
        if args.cases
        else None
    )

    with vort.CONFIG_PATH.open("r", encoding="utf-8") as f:
        grid = json.load(f)["grid"]
    print(
        f"Output: {out_dir} | grid={grid['nx']}x{grid['ny']} | steps={args.steps} | "
        f"streak last {args.streak_window} steps, sample_every={args.sample_every}"
    )

    summaries = []
    for case_id, slug, features in vort.CONTROL_CASES:
        if selected and case_id not in selected:
            continue
        summaries.append(
            run_streak_case(
                case_id,
                slug,
                features,
                out_dir=out_dir,
                total_steps=int(args.steps),
                streak_window=int(args.streak_window),
                sample_every=int(args.sample_every),
                report_every=int(args.report_every),
            )
        )

    manifest = {
        "grid": grid,
        "steps": int(args.steps),
        "streak_window_steps": int(args.streak_window),
        "sample_every": int(args.sample_every),
        "swap_action23_bodies": bool(vort.SWAP_ACTION23_BODIES),
        "cases": summaries,
    }
    manifest_path = out_dir / "manifest.json"
    with manifest_path.open("w", encoding="utf-8") as f:
        json.dump(manifest, f, indent=2)
    print(f"Manifest: {manifest_path}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
