# tests/run_sah04_case9_flow_diagnostics.py
"""Sah04 matrix case 9: velocity profiles at several x and full-field vorticity.

Loads geometry and setup from ``tests/tests/run_sah04_st_matrix.py``, runs one
simulation to the last LBM step, then writes PNG + JSON under
``tests/output/sah04_case9_flow_diag/`` (default).

Usage::
    conda run -n pycuda_3_10 python tests/run_sah04_case9_flow_diagnostics.py
    conda run -n pycuda_3_10 python tests/run_sah04_case9_flow_diagnostics.py --steps 60000 --burn 20000

Design::
    Intended for inlet / channel diagnostics after boundary fixes; not part of
    the St matrix gate.
"""

from __future__ import annotations

import argparse
import importlib.util
import json
import os
import shutil
import sys
import tempfile
from typing import Any, Dict, List, Sequence

import numpy as np
import pycuda.driver as cuda

_REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
_mtx_path = os.path.join(_REPO, "tests", "tests", "run_sah04_st_matrix.py")
_spec = importlib.util.spec_from_file_location("sah04_mtx", _mtx_path)
if _spec is None or _spec.loader is None:
    raise RuntimeError(f"Cannot load {_mtx_path}")
sah = importlib.util.module_from_spec(_spec)
sys.modules["sah04_mtx"] = sah
_spec.loader.exec_module(sah)


def _inlet_parabolic_ref_u(y: np.ndarray, ny: int, u0_mean: float) -> np.ndarray:
    """Match ``inlet_target_u`` in ``inlet_outlet.cuh`` (parabolic, mean ``u0_mean``)."""
    h = max(float(ny - 2), 1.0)
    yc = np.clip(y.astype(np.float64), 1.0, float(ny - 2))
    eta = (yc - 0.5) / h
    shape = np.maximum(0.0, 4.0 * eta * (1.0 - eta))
    return u0_mean * 1.5 * shape


def _default_x_indices(nx: int) -> List[int]:
    raw = [1, 2, 3, 5, 10, 20, 40, 80, 160, 320, 640, 960, 1200, 1600, 2000, 2200]
    out: List[int] = []
    for x in raw:
        if 1 <= x < nx - 1:
            out.append(int(x))
    mid = max(1, (nx - 2) // 2)
    if mid not in out:
        out.append(mid)
    out = sorted(set(out))
    return out


def _plot_profiles(
    out_png: str,
    *,
    ux: np.ndarray,
    uy: np.ndarray,
    x_indices: Sequence[int],
    ny: int,
    u0_mean: float,
    title: str,
) -> None:
    import matplotlib

    matplotlib.use("Agg")
    import matplotlib.pyplot as plt

    y = np.arange(ny, dtype=np.float64)
    u_ref = _inlet_parabolic_ref_u(y, ny, u0_mean)

    fig, axes = plt.subplots(1, 2, figsize=(12, 6), sharey=True)
    for xi in x_indices:
        axes[0].plot(ux[:, xi], y, label=f"x={xi}", linewidth=1.2)
        axes[1].plot(uy[:, xi], y, label=f"x={xi}", linewidth=1.2)
    axes[0].plot(u_ref, y, "k--", linewidth=1.5, label="inlet parabolic ref")
    axes[0].set_xlabel("u_x")
    axes[0].set_ylabel("y (lattice)")
    axes[0].set_title("u_x (y) at selected x")
    axes[0].legend(loc="best", fontsize=7)
    axes[0].grid(True, alpha=0.3)
    axes[1].set_xlabel("u_y")
    axes[1].set_title("u_y (y) at selected x")
    axes[1].legend(loc="best", fontsize=7)
    axes[1].grid(True, alpha=0.3)
    fig.suptitle(title)
    fig.tight_layout()
    os.makedirs(os.path.dirname(os.path.abspath(out_png)) or ".", exist_ok=True)
    fig.savefig(out_png, dpi=150, bbox_inches="tight")
    plt.close(fig)


def main() -> int:
    ap = argparse.ArgumentParser(description="Sah04 case 9 flow: profiles + vorticity")
    ap.add_argument("--collision", default="MRT", help="SRT, TRT, or MRT")
    ap.add_argument("--steps", type=int, default=60_000)
    ap.add_argument("--burn", type=int, default=20_000)
    ap.add_argument("--record-every", type=int, default=5)
    ap.add_argument("--outlet", default="neq_extrap")
    ap.add_argument(
        "--out-dir",
        default=os.path.join(_REPO, "tests", "output", "sah04_case9_flow_diag"),
        help="Output directory for PNG + JSON",
    )
    ap.add_argument(
        "--x-list",
        type=str,
        default="",
        help='Comma-separated x indices (default: built-in list); e.g. "1,2,5,40,200"',
    )
    args = ap.parse_args()

    mc = next(c for c in sah.MATRIX if c.case_id == 9)
    tier = sah._TIERS[mc.tier]
    ny = int(tier["ny"])
    nx = int(sah._NX)
    center = (sah._CX, float(tier["center_y"]))
    u_max = 0.1
    u0_mean = u_max / 1.5
    nu = u_max * float(sah._D) / float(mc.re)

    if args.x_list.strip():
        x_indices = [int(s.strip()) for s in args.x_list.split(",") if s.strip()]
        x_indices = [x for x in x_indices if 1 <= x < nx - 1]
    else:
        x_indices = _default_x_indices(nx)

    lbm_path = sah._DEFAULT_LBM
    cfg = sah._load_json(lbm_path)
    cfg["grid"]["nx"] = nx
    cfg["grid"]["ny"] = ny
    cfg["grid"]["nz"] = 1
    cfg["physics"]["viscosity"] = float(nu)
    cfg["physics"]["velocity"] = float(u0_mean)
    cfg["physics"]["rho"] = 1.0
    cfg["method"]["collision"] = str(args.collision).upper()
    cfg["method"]["streaming"] = "double_buffer"
    cfg["method"]["les"]["enabled"] = False
    cfg["method"]["outlet"]["mode"] = args.outlet

    body_doc = {
        "objects": [
            {
                "type": "cylinder",
                "center": list(center),
                "radius": float(sah._R_CYL),
            }
        ]
    }

    tmpd = tempfile.mkdtemp(prefix="celeris_sah09_diag_")
    lbm_tmp = os.path.join(tmpd, "config_lbm.json")
    body_tmp = os.path.join(tmpd, "config_body.json")
    sah._write_json(lbm_tmp, cfg)
    sah._write_json(body_tmp, body_doc)

    os.makedirs(args.out_dir, exist_ok=True)
    png_profiles = os.path.join(args.out_dir, "case9_profiles_ux_uy.png")
    png_vort = os.path.join(args.out_dir, "case9_omega_z_full.png")
    json_path = os.path.join(args.out_dir, "case9_profiles.json")

    try:
        from CelerisLab import Simulation  # noqa: WPS433

        sim = Simulation(lbm_config_path=lbm_tmp, body_config_path=body_tmp)
        sim.initialize()
        stream = cuda.Stream()
        rec_every = max(1, int(args.record_every))
        steps = int(args.steps)

        for step in range(1, steps + 1):
            sim.bodies.zero_force_segment_async(stream)
            sim.stepper.step(
                1,
                action_gpu=sim.bodies.action_gpu,
                obs_gpu=sim.bodies.obs_gpu,
                stream=stream,
            )

        stream.synchronize()
        macro = sim.get_macroscopic()
        ux = np.asarray(macro["ux"], dtype=np.float64)
        uy = np.asarray(macro["uy"], dtype=np.float64)
        sim.close()

        y = np.arange(ny, dtype=np.float64)
        u_ref = _inlet_parabolic_ref_u(y, ny, u0_mean)

        title = (
            f"Sah04 case 9 {args.collision} Re={mc.re} ny={ny} "
            f"steps={steps} (last step)"
        )
        _plot_profiles(png_profiles, ux=ux, uy=uy, x_indices=x_indices, ny=ny, u0_mean=u0_mean, title=title)
        sah.save_final_vorticity_png(png_vort, macro["ux"], macro["uy"], title=title + " omega_z")

        profiles: Dict[str, Any] = {
            "case_id": 9,
            "collision": str(args.collision).upper(),
            "Re": mc.re,
            "nx": nx,
            "ny": ny,
            "steps": steps,
            "burn_requested": int(args.burn),
            "u_max": u_max,
            "u0_mean_parabolic": u0_mean,
            "x_indices": list(x_indices),
            "y_lattice": y.tolist(),
            "inlet_parabolic_ref_ux": u_ref.tolist(),
            "profiles": {},
        }
        for xi in x_indices:
            profiles["profiles"][str(xi)] = {
                "ux": ux[:, xi].tolist(),
                "uy": uy[:, xi].tolist(),
            }
        with open(json_path, "w", encoding="utf-8") as f:
            json.dump(profiles, f, indent=2)

        print("Wrote:", png_profiles, flush=True)
        print("Wrote:", png_vort, flush=True)
        print("Wrote:", json_path, flush=True)
    finally:
        shutil.rmtree(tmpd, ignore_errors=True)

    return 0


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