# target_simulator/core/command_builder.py

"""
Constructs MMI command strings based on high-level data models.
"""

import json
from typing import Dict, Any, List, Optional

from .models import Target, Waypoint, ManeuverType
from target_simulator.utils.logger import get_logger

logger = get_logger(__name__)

# --- Constants for JSON protocol unit conversion ---
NM_TO_M = 1852.0
FT_TO_M = 0.3048
FPS_TO_MPS = 0.3048


def build_json_update(targets: List[Target], global_cmd: Optional[str] = None) -> str:
    """
    Builds a JSON string for atomically updating multiple targets.

    Args:
        targets: A list of Target objects to include in the update.
        global_cmd: An optional global command string (e.g., "reset").

    Returns:
        A formatted JSON string representing the bulk update command.
    """
    payload: Dict[str, Any] = {"TGTS": []}

    if global_cmd:
        payload["CMD"] = global_cmd

    for target in targets:
        target_dict = {
            "UID": target.target_id + 100,
            "ID": target.target_id,
            "a": int(target.active),  # 1 for active, 0 for inactive
            "t": int(target.traceable),  # 1 for traceable, 0 for not traceable
            "R": target.current_range_nm * NM_TO_M,
            "NAZ": target.current_azimuth_deg,
            "GV": target.current_velocity_fps * FPS_TO_MPS,
            "VV": 0.0,  # Vertical velocity, set to 0 as per initial spec
            "H": target.current_heading_deg,
            "A": target.current_altitude_ft * FT_TO_M,
            "CMD": "CONTINUE" if target.active else "STOP",
        }
        payload["TGTS"].append(target_dict)

    # Use indent for readability in debug files
    return json.dumps(payload, indent=4)


def build_tgtinit(target: Target) -> str:
    """
    Builds the 'tgtinit' command from a target's initial state.
    After the refactor, the target object's state is pre-calculated
    by reset_simulation(), so we can read kinematics directly.
    """
    params = [
        target.target_id,
        f"{target.current_range_nm:.2f}",
        f"{target.current_azimuth_deg:.2f}",
        f"{target.current_velocity_fps:.2f}",
        f"{target.current_heading_deg:.2f}",
        f"{target.current_altitude_ft:.2f}",
    ]
    qualifiers = ["/s" if target.active else "/-s", "/t" if target.traceable else "/-t"]
    if hasattr(target, "restart") and getattr(target, "restart", False):
        qualifiers.append("/r")

    command_parts = ["tgtinit"] + [str(p) for p in params] + qualifiers
    full_command = " ".join(command_parts)

    logger.debug(f"Built command: {full_command!r}")
    return full_command


def build_tgtset_from_target_state(target: Target, include_flags: bool = False) -> str:
    """
    Builds the 'tgtset' command from a target's CURRENT dynamic state.
    This is used for continuous updates during simulation.
    """
    params = [
        target.target_id,
        f"{target.current_range_nm:.2f}",
        f"{target.current_azimuth_deg:.2f}",
        f"{target.current_velocity_fps:.2f}",
        f"{target.current_heading_deg:.2f}",
        f"{target.current_altitude_ft:.2f}",
    ]
    # For live tgtset updates we normally only send the positional/kinematic
    # parameters. However, callers can request inclusion of qualifiers
    # (/s, /-s, /t, /-t, /r) by setting include_flags=True (used by debug UI).
    command_parts = ["tgtset"] + [str(p) for p in params]
    if include_flags:
        if getattr(target, "active", False):
            command_parts.append("/s")
        else:
            command_parts.append("/-s")
        if getattr(target, "traceable", False):
            command_parts.append("/t")
        else:
            command_parts.append("/-t")
        if hasattr(target, "restart") and getattr(target, "restart", False):
            command_parts.append("/r")
    full_command = " ".join(command_parts)

    logger.debug(f"Built command: {full_command!r}")
    return full_command


def build_tgtset_selective(target_id: int, updates: Dict[str, Any]) -> str:
    """
    Builds a 'tgtset' command with skippable parameters ('*').
    """
    param_order = [
        "range_nm",
        "azimuth_deg",
        "velocity_fps",
        "heading_deg",
        "altitude_ft",
    ]
    params = [str(updates.get(key, "*")) for key in param_order]
    command_parts = ["tgtset", str(target_id)] + params

    qualifiers = []
    if "active" in updates:
        qualifiers.append("/s" if updates["active"] else "/-s")
    if "traceable" in updates:
        qualifiers.append("/t" if updates["traceable"] else "/-t")
    # Support restart qualifier for selective commands when requested
    if "restart" in updates and updates.get("restart"):
        qualifiers.append("/r")
    command_parts.extend(qualifiers)

    full_command = " ".join(command_parts)
    logger.debug(f"Built selective command: {full_command!r}")
    return full_command


# --- Simple Command Builders ---


def build_tgtreset(target_id: int = -1) -> str:
    """Builds the 'tgtreset' command. ID -1 resets all targets."""
    return f"tgtreset {target_id}"


def build_pause() -> str:
    """Builds the 'pause' command."""
    return "pause"


def build_continue() -> str:
    """Builds the 'continue' command."""
    return "continue"


def build_aclatch() -> str:
    """Builds the 'aclatch' command to latch A/C data."""
    return "aclatch"


def build_acunlatch() -> str:
    """Builds the command to release the A/C data latch."""
    return "acunlatch"


def build_reset() -> str:
    """Builds the 'reset' command."""
    return "reset"


def build_json_reset_ids(max_id: int = 31, max_bytes: int = 1020) -> list[str]:
    """
    Build one or more compact JSON payload strings that contain target entries
    for IDs 0..max_id with all numeric fields zeroed and flags cleared. This
    mirrors the behaviour of the SFP debug window's "Reset IDs" action and
    returns a list of newline-terminated compact JSON payloads that fit within
    the provided transport size limit.

    Args:
        max_id: Highest target id to include (inclusive). Defaults to 31.
        max_bytes: Maximum bytes allowed per payload (transport limit).

    Returns:
        List of JSON payload strings (each ends with '\n').
    """
    targets = []
    for tid in range(0, max_id + 1):
        wp = Waypoint(
            maneuver_type=ManeuverType.FLY_TO_POINT,
            target_range_nm=0.0,
            target_azimuth_deg=0.0,
            target_altitude_ft=0.0,
            target_velocity_fps=0.0,
            target_heading_deg=0.0,
        )
        t = Target(target_id=tid, trajectory=[wp])
        t.active = False
        t.traceable = False
        t.restart = False
        t.current_range_nm = 0.0
        t.current_azimuth_deg = 0.0
        t.current_velocity_fps = 0.0
        t.current_heading_deg = 0.0
        t.current_altitude_ft = 0.0
        targets.append(t)

    import json as _json

    def build_compact_payload(tlist: list[Target]) -> str:
        s = build_json_update(tlist)
        try:
            obj = _json.loads(s)
            compact = _json.dumps(obj, separators=(",", ":"))
        except Exception:
            compact = s.replace("\n", "").replace("\r", "")
        return compact

    n = len(targets)
    batch_size = n
    while batch_size > 0:
        payload = build_compact_payload(targets[:batch_size])
        if len(payload.encode("utf-8")) <= max_bytes:
            break
        batch_size -= 1

    if batch_size == 0:
        # As a defensive fallback, return the full list as a single (possibly
        # oversize) payload so callers can attempt to send it and handle errors.
        payload = build_compact_payload(targets)
        if not payload.endswith("\n"):
            payload = payload + "\n"
        return [payload]

    payloads: list[str] = []
    i = 0
    while i < n:
        j = min(i + batch_size, n)
        p = build_compact_payload(targets[i:j])
        if not p.endswith("\n"):
            p = p + "\n"
        payloads.append(p)
        i = j

    return payloads
