"""
gs_receiver.py — GoldSim → Python time series receiver
========================================================
Called by GoldSim each timestep via GSPy_FromGS.dll.

Accepts 4 scalar time series and 1 vector (in the order defined in GSPy_FromGS.json):
    1. Diversion
    2. Reservoir_Volume
    3. Irrigation_Supply
    4. Municipal_Supply
    5. Water_Use_Vector (length 5)

Each time GoldSim calls this function the full time series up to the
current simulation time is passed.  The script converts it to a clean
JSON structure and writes it to chart_data.json in the same folder.

Display metadata (incoming_unit, unit, total_unit, display_scale) is read from
GSPy_FromGS.json input entries and stamped onto each series so DynamicGUI
can label / scale charts without a hardcoded map.

GSPy time series dictionary keys:
    timestamps  – 1-D NumPy array
                  • time_basis == 0  → elapsed time (same units as model)
                  • time_basis == 1  → Excel serial dates (days since 1900-01-01)
    data        – 1-D NumPy array, shape (num_time_points,)
    time_basis  – float  0 = elapsed, 1 = calendar
    data_type   – float  (GoldSim internal type tag; not needed for charting)
"""

import json
import os
import traceback
from datetime import datetime, timedelta, timezone
from typing import Any

import numpy as np
import gspy

# chart_data.json is written to the same folder as this script.
_DIR = os.path.dirname(os.path.abspath(__file__))
CHART_DATA_FILE = os.path.join(_DIR, "chart_data.json")
FROM_GS_CONFIG = os.path.join(_DIR, "GSPy_FromGS.json")

# Fallback names if GSPy_FromGS.json cannot be read
_DEFAULT_SERIES_NAMES = [
    "Diversion",
    "Reservoir_Volume",
    "Irrigation_Supply",
    "Municipal_Supply",
]


def _load_from_gs_meta() -> tuple[list[str], dict[str, dict[str, Any]]]:
    """
    Read optional display metadata from GSPy_FromGS.json.
    Returns (timeseries_names_in_order, meta_by_name).
    Unknown keys (incoming_unit, unit, total_unit, display_scale) are ignored by the GSPy DLL.
    """
    meta_by_name: dict[str, dict[str, Any]] = {}
    names: list[str] = []
    try:
        with open(FROM_GS_CONFIG, encoding="utf-8") as f:
            cfg = json.load(f)
        for entry in cfg.get("inputs") or []:
            name = str(entry.get("name") or "").strip()
            if not name:
                continue
            typ = str(entry.get("type") or "timeseries").lower()
            meta: dict[str, Any] = {"type": typ}
            incoming = entry.get("incoming_unit") or entry.get("source_unit")
            if incoming:
                meta["incoming_unit"] = str(incoming).strip()
            unit = entry.get("unit") or entry.get("display_unit")
            if unit:
                meta["unit"] = str(unit).strip()
            total_unit = entry.get("total_unit")
            if total_unit:
                meta["total_unit"] = str(total_unit).strip()
            scale = entry.get("display_scale")
            if scale is not None and str(scale).strip() != "":
                try:
                    meta["display_scale"] = float(scale)
                except (TypeError, ValueError):
                    pass
            meta_by_name[name] = meta
            if typ in ("timeseries", "time_series"):
                names.append(name)
    except Exception as exc:
        gspy.log(f"Could not read {FROM_GS_CONFIG}: {exc}", 1)

    if not names:
        names = list(_DEFAULT_SERIES_NAMES)
    return names, meta_by_name


# ---------------------------------------------------------------------------
# Timestamp conversion helpers
# ---------------------------------------------------------------------------

def _excel_serial_to_iso(serial: float) -> str:
    """
    Convert a GoldSim/Excel calendar timestamp to YYYY-MM-DD.

    GoldSim may pass either:
      • Excel serial days since 1899-12-30 (typical ~40_000–60_000 for 2010–2060)
      • Seconds since 1899-12-30 (e.g. 3_952_627_200 for 2025-04-01)
    """
    try:
        value = float(serial)
        if abs(value) >= 1e6:
            dt = datetime(1899, 12, 30, tzinfo=timezone.utc) + timedelta(seconds=value)
        else:
            dt = datetime(1899, 12, 30, tzinfo=timezone.utc) + timedelta(days=value)
        return dt.strftime("%Y-%m-%d")
    except Exception:
        return str(serial)


def _convert_timestamps(timestamps: np.ndarray, time_basis: float) -> list:
    """Return a JSON-serialisable list of timestamp labels."""
    if time_basis == 1.0:
        # Calendar dates — convert Excel serial → ISO string
        return [_excel_serial_to_iso(t) for t in timestamps.tolist()]
    else:
        # Elapsed time — keep as plain numbers
        return [round(float(t), 6) for t in timestamps.tolist()]


def _series_to_dict(ts_dict: dict, name: str, meta: dict[str, Any] | None = None) -> dict[str, Any]:
    """Convert one GSPy time series dictionary to a chart-ready dict."""
    time_basis: float = float(ts_dict.get("time_basis", 0.0))
    timestamps: np.ndarray = ts_dict["timestamps"]
    data: np.ndarray = ts_dict["data"]
    meta = meta or {}

    out: dict[str, Any] = {
        "name": name,
        "time_basis": "calendar" if time_basis == 1.0 else "elapsed",
        "timestamps": _convert_timestamps(timestamps, time_basis),
        "values": [round(float(v), 6) for v in data.tolist()],
        "point_count": int(data.shape[0]),
    }
    if meta.get("incoming_unit"):
        out["incoming_unit"] = meta["incoming_unit"]
    if meta.get("unit"):
        out["unit"] = meta["unit"]
    if meta.get("total_unit"):
        out["total_unit"] = meta["total_unit"]
    if "display_scale" in meta:
        out["display_scale"] = meta["display_scale"]
    return out


# ---------------------------------------------------------------------------
# Main entry point called by GSPy
# ---------------------------------------------------------------------------

def process_data(*args):
    """
    Receives timeseries (+ optional vector) from GoldSim and writes chart_data.json.
    Returns status 1.0.
    """
    try:
        series_names, meta_by_name = _load_from_gs_meta()
        if len(args) < len(series_names):
            gspy.log(
                f"Expected at least {len(series_names)} timeseries inputs, got {len(args)}.",
                1,
            )

        series_list = []
        for i, name in enumerate(series_names):
            if i >= len(args):
                break
            ts_dict = args[i]
            series = _series_to_dict(ts_dict, name, meta_by_name.get(name))
            series_list.append(series)
            gspy.log(
                f"{name}: {series['point_count']} points, "
                f"time_basis={series['time_basis']}"
                + (f", unit={series.get('unit')}" if series.get("unit") else ""),
                2,
            )

        water_use_vector = []
        vector_meta = next(
            (m for n, m in meta_by_name.items() if m.get("type") == "vector"),
            None,
        )
        vector_name = next(
            (n for n, m in meta_by_name.items() if m.get("type") == "vector"),
            "Water_Use_Vector",
        )
        if len(args) > len(series_names):
            water_use_vector = [round(float(v), 2) for v in args[len(series_names)].tolist()]
            gspy.log(f"Received {vector_name}: {water_use_vector}", 2)

        payload: dict[str, Any] = {
            "last_updated": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
            "series": {s["name"]: s for s in series_list},
            "water_use_vector": water_use_vector,
        }
        if vector_meta and water_use_vector is not None:
            vec_info: dict[str, Any] = {"name": vector_name, "values": water_use_vector}
            if vector_meta.get("incoming_unit"):
                vec_info["incoming_unit"] = vector_meta["incoming_unit"]
            if vector_meta.get("unit"):
                vec_info["unit"] = vector_meta["unit"]
            if "display_scale" in vector_meta:
                vec_info["display_scale"] = vector_meta["display_scale"]
            payload["vectors"] = {vector_name: vec_info}

        with open(CHART_DATA_FILE, "w") as f:
            json.dump(payload, f, indent=2)

        gspy.log(f"chart_data.json updated ({len(series_list)} series).", 2)

        # Return status 1.0 = success
        return (1.0,)

    except Exception:
        gspy.log("Error in gs_receiver.process_data:\n" + traceback.format_exc(), 0)
        return (0.0,)
