#!/usr/bin/env python3
"""
Interactive BABYLIN animation validation for ALM_Req_A.

This script executes the requirement-oriented checks step-by-step and pauses
after each action so the tester can verify physical LED behavior.

Covered checks:
1) AmbLightMode behavior (0 immediate, 1 fade RGBI, 2 immediate color + fade I)
2) AmbLightUpdate save/apply/discard
3) AmbLightDuration scaling (0.2 s/LSB)
4) LID range selection (single-node, broadcast, invalid From>To)
"""

import argparse
import logging
import time

from pylin import LinBusManager, LinDevice22
from pymumclient import MelexisUniversalMaster

from config import *


logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)-15s %(levelname)-8s %(message)s",
)
logger = logging.getLogger(__name__)

SEPARATOR = "=" * 78
SUB = "-" * 78

# ALM_Status.ALMLedState values
LED_STATE_OFF = 0
LED_STATE_ANIMATING = 1
LED_STATE_ON = 2
LED_STATE_NAMES = {
    LED_STATE_OFF: "OFF",
    LED_STATE_ANIMATING: "ANIMATING",
    LED_STATE_ON: "ON",
}


def pause(msg):
    print()
    input(f">>> {msg}")
    print()


def banner(title):
    logger.info(SEPARATOR)
    logger.info(title)
    logger.info(SEPARATOR)


def section(title):
    logger.info(SUB)
    logger.info(title)
    logger.info(SUB)


def read_alm_status(lin_dev):
    """Return (parsed_dict, raw_bytes) or (None, None)."""
    try:
        response = lin_dev.send_message(
            master_to_slave=False,
            frame_id=ALM_STATUS_FRAME["frame_id"],
            data_length=ALM_STATUS_FRAME["length"],
            data=None,
        )
        if response and len(response) >= ALM_STATUS_FRAME["length"]:
            return unpack_frame(ALM_STATUS_FRAME, response), response
        return None, None
    except Exception as exc:
        logger.error("Failed reading ALM_Status: %s", exc)
        return None, None


def read_led_state(lin_dev):
    parsed, _ = read_alm_status(lin_dev)
    if parsed is None:
        return -1
    return parsed.get("ALMLEDState", -1)


def read_nad(lin_dev, fallback):
    parsed, raw = read_alm_status(lin_dev)
    if parsed is None:
        logger.warning("Could not read ALM_Status, fallback NAD=0x%02X", fallback)
        return fallback
    nad = parsed.get("ALMNadNo", fallback)
    logger.info("Detected ALMNadNo=0x%02X (raw: %s)", nad, " ".join(f"{b:02X}" for b in raw))
    return nad


def send_req(
    lin_dev,
    *,
    red,
    green,
    blue,
    intensity,
    update,
    mode,
    duration,
    lid_from,
    lid_to,
):
    data = pack_frame(
        ALM_REQ_A_FRAME,
        AmbLightColourRed=red,
        AmbLightColourGreen=green,
        AmbLightColourBlue=blue,
        AmbLightIntensity=intensity,
        AmbLightUpdate=update,
        AmbLightMode=mode,
        AmbLightDuration=duration,
        AmbLightLIDFrom=lid_from,
        AmbLightLIDTo=lid_to,
    )
    lin_dev.send_message(
        master_to_slave=True,
        frame_id=ALM_REQ_A_FRAME["frame_id"],
        data_length=ALM_REQ_A_FRAME["length"],
        data=data,
    )


def observe_state(lin_dev, seconds):
    """Poll status slowly and print changes."""
    logger.info("Observing for %.1f s...", seconds)
    end_t = time.time() + seconds
    last = None
    while time.time() < end_t:
        st = read_led_state(lin_dev)
        if st != last:
            name = LED_STATE_NAMES.get(st, f"UNKNOWN({st})")
            logger.info("  ALMLEDState -> %s", name)
            last = st
        time.sleep(0.25)


def guided_step(lin_dev, title, expectation_lines, command_kwargs, observe_s):
    section(title)
    logger.info("What you should see:")
    for line in expectation_lines:
        logger.info("  - %s", line)
    pause("Press Enter to send this command...")
    send_req(lin_dev, **command_kwargs)
    observe_state(lin_dev, observe_s)
    pause("Verify visually, then press Enter for the next step...")


def main():
    parser = argparse.ArgumentParser(description="Interactive ALM animation checks for BABYLIN")
    parser.add_argument("--host", default=MUM_HOST, help=f"MUM IP (default: {MUM_HOST})")
    parser.add_argument(
        "--nad",
        type=lambda x: int(x, 0),
        default=LED_DEFAULT_NAD,
        help=f"Fallback NAD if ALM_Status read fails (default: 0x{LED_DEFAULT_NAD:02X})",
    )
    parser.add_argument(
        "--slow-factor",
        type=float,
        default=1.0,
        help="Multiply wait/observe durations (default: 1.0)",
    )
    args = parser.parse_args()

    mum = None
    linmaster = None
    lin_dev = None

    try:
        banner("Connecting to MUM / LIN")
        mum = MelexisUniversalMaster()
        mum.open_all(args.host)

        power_control = mum.get_device(MUM_POWER_DEVICE)
        linmaster = mum.get_device(MUM_LIN_DEVICE)
        linmaster.setup()

        lin_bus = LinBusManager(linmaster)
        lin_dev = LinDevice22(lin_bus)
        lin_dev.baudrate = LIN_BAUDRATE
        lin_dev.nad = args.nad

        power_control.power_up()
        time.sleep(0.5 * args.slow_factor)

        nad = read_nad(lin_dev, args.nad)
        lin_dev.nad = nad

        banner("Interactive Requirement Validation")
        logger.info("Target NAD: 0x%02X", nad)
        logger.info("Slow factor: %.2f", args.slow_factor)
        logger.info("You will be prompted before and after every test step.")
        pause("Press Enter to start from a known OFF baseline...")

        # Step 0: Baseline OFF
        guided_step(
            lin_dev,
            "Step 0 - Baseline OFF",
            [
                "LED should turn OFF quickly.",
                "ALMLEDState should become OFF.",
            ],
            {
                "red": 0,
                "green": 0,
                "blue": 0,
                "intensity": 0,
                "update": 0,
                "mode": 0,
                "duration": 0,
                "lid_from": nad,
                "lid_to": nad,
            },
            1.0 * args.slow_factor,
        )

        # 1) Mode behavior checks
        guided_step(
            lin_dev,
            "Step 1 - Mode 0 Immediate Setpoint",
            [
                "Color/intensity should change immediately.",
                "No visible fade; direct jump to requested setpoint.",
            ],
            {
                "red": 0,
                "green": 180,
                "blue": 80,
                "intensity": 200,
                "update": 0,
                "mode": 0,
                "duration": 10,
                "lid_from": nad,
                "lid_to": nad,
            },
            1.0 * args.slow_factor,
        )

        guided_step(
            lin_dev,
            "Step 2 - Mode 1 Fade RGB + Intensity (2.0 s)",
            [
                "RGB and intensity should both transition smoothly.",
                "Transition duration should be close to 2.0 s (Duration=10).",
            ],
            {
                "red": 255,
                "green": 40,
                "blue": 0,
                "intensity": 220,
                "update": 0,
                "mode": 1,
                "duration": 10,
                "lid_from": nad,
                "lid_to": nad,
            },
            3.0 * args.slow_factor,
        )

        guided_step(
            lin_dev,
            "Step 3 - Mode 2 Immediate Color + Faded Intensity (2.0 s)",
            [
                "Color should jump immediately to the new RGB target.",
                "Only intensity should ramp over ~2.0 s.",
            ],
            {
                "red": 0,
                "green": 0,
                "blue": 255,
                "intensity": 50,
                "update": 0,
                "mode": 2,
                "duration": 10,
                "lid_from": nad,
                "lid_to": nad,
            },
            3.0 * args.slow_factor,
        )

        # 2) Update save/apply/discard checks
        guided_step(
            lin_dev,
            "Step 4 - Update=1 Save (must NOT apply)",
            [
                "LED output should remain unchanged after this command.",
                "No visible color/intensity change should occur.",
            ],
            {
                "red": 0,
                "green": 255,
                "blue": 0,
                "intensity": 255,
                "update": 1,
                "mode": 1,
                "duration": 10,
                "lid_from": nad,
                "lid_to": nad,
            },
            1.5 * args.slow_factor,
        )

        guided_step(
            lin_dev,
            "Step 5 - Update=2 Apply Saved",
            [
                "Saved command from Step 4 should execute now.",
                "Payload in this Apply frame should be ignored by ECU logic.",
                "You should see saved behavior (mode/duration/RGBI from Step 4).",
            ],
            {
                "red": 7,
                "green": 7,
                "blue": 7,
                "intensity": 7,
                "update": 2,
                "mode": 0,
                "duration": 0,
                "lid_from": nad,
                "lid_to": nad,
            },
            3.0 * args.slow_factor,
        )

        guided_step(
            lin_dev,
            "Step 6 - Update=3 Discard Saved",
            [
                "Saved buffer should be cleared.",
                "This discard command itself should not change output.",
            ],
            {
                "red": 0,
                "green": 0,
                "blue": 0,
                "intensity": 0,
                "update": 3,
                "mode": 0,
                "duration": 0,
                "lid_from": nad,
                "lid_to": nad,
            },
            1.5 * args.slow_factor,
        )

        guided_step(
            lin_dev,
            "Step 7 - Update=2 After Discard",
            [
                "No saved command should exist now.",
                "Apply should behave like a no-op (no new visible action).",
            ],
            {
                "red": 123,
                "green": 12,
                "blue": 45,
                "intensity": 200,
                "update": 2,
                "mode": 1,
                "duration": 5,
                "lid_from": nad,
                "lid_to": nad,
            },
            2.0 * args.slow_factor,
        )

        # 3) Duration scaling checks
        guided_step(
            lin_dev,
            "Step 8 - Duration=1 (expect ~0.2 s)",
            [
                "Transition should complete very quickly (~0.2 s).",
            ],
            {
                "red": 255,
                "green": 0,
                "blue": 0,
                "intensity": 200,
                "update": 0,
                "mode": 1,
                "duration": 1,
                "lid_from": nad,
                "lid_to": nad,
            },
            1.5 * args.slow_factor,
        )

        guided_step(
            lin_dev,
            "Step 9 - Duration=5 (expect ~1.0 s)",
            [
                "Transition should take around 1.0 s.",
                "Visibly slower than Step 8.",
            ],
            {
                "red": 0,
                "green": 255,
                "blue": 0,
                "intensity": 200,
                "update": 0,
                "mode": 1,
                "duration": 5,
                "lid_from": nad,
                "lid_to": nad,
            },
            2.0 * args.slow_factor,
        )

        guided_step(
            lin_dev,
            "Step 10 - Duration=10 (expect ~2.0 s)",
            [
                "Transition should take around 2.0 s.",
                "Visibly slower than Step 9.",
            ],
            {
                "red": 0,
                "green": 0,
                "blue": 255,
                "intensity": 200,
                "update": 0,
                "mode": 1,
                "duration": 10,
                "lid_from": nad,
                "lid_to": nad,
            },
            3.0 * args.slow_factor,
        )

        # 4) LID selection checks
        guided_step(
            lin_dev,
            "Step 11 - LID Single-Node Select (From=To=NAD)",
            [
                "This node should react (it is explicitly selected).",
            ],
            {
                "red": 255,
                "green": 120,
                "blue": 0,
                "intensity": 180,
                "update": 0,
                "mode": 0,
                "duration": 0,
                "lid_from": nad,
                "lid_to": nad,
            },
            1.0 * args.slow_factor,
        )

        guided_step(
            lin_dev,
            "Step 12 - LID Broadcast Select (From=0, To=255)",
            [
                "This node should react (broadcast range).",
            ],
            {
                "red": 120,
                "green": 0,
                "blue": 255,
                "intensity": 180,
                "update": 0,
                "mode": 0,
                "duration": 0,
                "lid_from": 0,
                "lid_to": 255,
            },
            1.0 * args.slow_factor,
        )

        guided_step(
            lin_dev,
            "Step 13 - LID Invalid Range (From > To)",
            [
                "Node should ignore this command.",
                "No visible output change is expected.",
            ],
            {
                "red": 255,
                "green": 255,
                "blue": 255,
                "intensity": 255,
                "update": 0,
                "mode": 0,
                "duration": 0,
                "lid_from": 20,
                "lid_to": 10,
            },
            1.5 * args.slow_factor,
        )

        pause("All checks done. Press Enter to send final OFF cleanup...")
        send_req(
            lin_dev,
            red=0,
            green=0,
            blue=0,
            intensity=0,
            update=0,
            mode=0,
            duration=0,
            lid_from=nad,
            lid_to=nad,
        )
        observe_state(lin_dev, 1.0 * args.slow_factor)
        banner("Test sequence completed")

    except KeyboardInterrupt:
        logger.info("Interrupted by user")
    finally:
        try:
            if lin_dev is not None:
                # Best effort: leave node OFF
                send_req(
                    lin_dev,
                    red=0,
                    green=0,
                    blue=0,
                    intensity=0,
                    update=0,
                    mode=0,
                    duration=0,
                    lid_from=lin_dev.nad,
                    lid_to=lin_dev.nad,
                )
        except Exception:
            pass
        try:
            if linmaster is not None:
                linmaster.teardown()
        except Exception:
            pass


if __name__ == "__main__":
    main()
