#!/usr/bin/env python3
"""
Simple Tkinter GUI + asyncio simulation runner.

- Shows driver current speed and RPM.
- Lets the user inject a spoofed telemetry packet (speed & rpm).
- Re-uses SpoofingProtector.sign(...) for HMAC generation.
"""
import threading
import asyncio
import time
import logging
from typing import Dict, Any
import tkinter as tk
from tkinter import ttk

# import protection logic from repo
from security.spoofing_protection import SpoofingProtector
from security.hijacking_protection import HijackingProtector
from security.engineer_interface import EngineerInterface

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s: %(message)s")

# Shared state between asyncio thread and GUI (protected by threading.Lock)
shared_state: Dict[str, Dict[str, Any]] = {}
state_lock = threading.Lock()

# Container to hold queue and loop references once created in async thread
_async_ctx = {"loop": None, "queue": None}


async def producer(queue: asyncio.Queue, stop_event: asyncio.Event):
    """
    Producer: periodically emits telemetry and control messages.
    This is a simplified version of the repo's simulated_message_source,
    and also will handle external GUI-injected spoof messages that are
    placed directly into the queue by the GUI (via run_coroutine_threadsafe).
    """
    driver_id = "driver_device_01"
    engineer_dash_id = "engineer_remote_01"
    outside_bad_actor = "unknown_remote_66"

    protector = SpoofingProtector()
    shared_secret = protector.secret

    # emit a few normal messages, then wait in a loop producing periodic telemetry
    i = 0
    while not stop_event.is_set():
        # normal telemetry
        telemetry = {"speed_mph": 140 + (i % 10), "rpm": 7000 + (i % 100)}
        await queue.put(("telemetry", driver_id, telemetry, 0.8, SpoofingProtector.sign(payload=telemetry, secret=shared_secret)))
        # engineer control occasionally
        if i % 6 == 0:
            control = {"cmd": "set_mode", "mode": "race_preset"}
            await queue.put(("control", engineer_dash_id, control, 0.9, SpoofingProtector.sign(payload=control, secret=shared_secret)))
        i += 1
        await asyncio.sleep(0.5)

    # send END marker when stopping
    await queue.put(("system", "simulator", {"event": "END"}, 1.0, ""))


async def consumer_loop(queue: asyncio.Queue, stop_event: asyncio.Event):
    """
    Consumer similar to main_loop in the repo. It inspects messages and updates shared_state
    so the GUI can display current driver values.
    """
    protector_hijack = HijackingProtector()
    protector_spoof = SpoofingProtector()
    engineer_iface = EngineerInterface()

    running = True
    while running and not stop_event.is_set():
        try:
            msg_type, source, payload, rssi, hmac_tag = await queue.get()
        except asyncio.CancelledError:
            break

        if msg_type == "system" and payload.get("event") == "END":
            running = False
            logging.info("Simulation complete.")
            continue

        # spoofing/integrity checks
        spoof_result = protector_spoof.inspect_message(msg_type=msg_type, source=source, payload=payload, hmac_tag=hmac_tag)

        # hijacking checks
        hijack_result = protector_hijack.observe_message(msg_type=msg_type, source=source, rssi=rssi)

        allow = True
        reason_blocks = []
        if not spoof_result["valid"]:
            allow = False
            reason_blocks.append(f"spoofing_invalid: {spoof_result['reason']}")
        if hijack_result.get("action") == "soft_lockdown_active":
            if msg_type == "control" and engineer_iface.is_engineer_payload(payload):
                token = payload.get("token")
                if engineer_iface.validate_token(token):
                    allow = True
                else:
                    allow = False
                    reason_blocks.append("engineer_token_invalid")
            else:
                allow = False
                reason_blocks.append("soft_lockdown_blocks_non_engineer")

        if not allow:
            logging.warning("Blocked message from %s: reasons=%s payload=%s", source, reason_blocks, payload)
            protector_hijack.record_blocked(source, reason_blocks)
        else:
            logging.info("Allowed message from %s: payload=%s", source, payload)
            # If telemetry from driver, update shared_state for GUI
            if msg_type == "telemetry":
                with state_lock:
                    cur = shared_state.setdefault(source, {})
                    # copy values we care about
                    if isinstance(payload, dict):
                        if "speed_mph" in payload:
                            cur["speed_mph"] = payload["speed_mph"]
                        if "rpm" in payload:
                            cur["rpm"] = payload["rpm"]
                    cur["last_update"] = time.time()

    logging.info("Consumer loop exiting.")


async def async_main(stop_event: asyncio.Event):
    loop = asyncio.get_running_loop()
    queue = asyncio.Queue()
    # publish queue & loop for GUI thread to use
    _async_ctx["loop"] = loop
    _async_ctx["queue"] = queue

    prod = asyncio.create_task(producer(queue, stop_event))
    cons = asyncio.create_task(consumer_loop(queue, stop_event))
    await asyncio.gather(prod, cons)


def start_async_thread():
    """
    Start the asyncio event loop in a background thread.
    Returns (thread, stop_event).
    """
    stop_event = asyncio.Event()

    def thread_target():
        # each thread needs its own event loop
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)
        # We will use loop.run_until_complete to run async_main until finished.
        # Provide a stop mechanism via an asyncio.Event we create on this loop.
        local_stop = asyncio.Event()
        # Store the local_stop so main thread can set it via call_soon_threadsafe
        _async_ctx["local_stop"] = local_stop
        try:
            loop.run_until_complete(async_main(local_stop))
        finally:
            loop.close()

    th = threading.Thread(target=thread_target, daemon=True)
    th.start()
    # wait until queue & loop are available (producer sets them)
    while _async_ctx.get("loop") is None or _async_ctx.get("queue") is None:
        time.sleep(0.05)
    return th


def stop_async_thread():
    """
    Signal the asyncio thread to stop by setting the loop-local asyncio.Event.
    """
    loop = _async_ctx.get("loop")
    local_stop = _async_ctx.get("local_stop")
    if loop and local_stop:
        # schedule setting the event from the other thread
        loop.call_soon_threadsafe(local_stop.set)


def inject_spoofed_telemetry(speed: float, rpm: int):
    """
    Called by GUI to inject a spoofed telemetry packet into the simulation queue.
    Uses SpoofingProtector.sign(...) to build a valid HMAC for the simulation's protector secret.
    """
    loop = _async_ctx.get("loop")
    queue = _async_ctx.get("queue")
    if not loop or not queue:
        logging.error("Simulation not running; cannot inject.")
        return

    protector = SpoofingProtector()
    payload = {"speed_mph": float(speed), "rpm": int(rpm)}
    tag = SpoofingProtector.sign(payload=payload, secret=protector.secret)
    # schedule queue.put on the event loop thread
    fut = asyncio.run_coroutine_threadsafe(queue.put(("telemetry", "driver_device_01", payload, 0.3, tag)), loop)
    try:
        fut.result(timeout=1.0)
    except Exception as exc:
        logging.error("Failed to inject telemetry: %s", exc)


class SimpleGUI:
    def __init__(self, root: tk.Tk):
        self.root = root
        root.title("AI-Race-Strategist - GUI")
        self.create_widgets()
        self.update_ui_loop()  # start polling loop

    def create_widgets(self):
        frm = ttk.Frame(self.root, padding=12)
        frm.grid()

        ttk.Label(frm, text="Driver device: driver_device_01").grid(column=0, row=0, sticky="w")

        self.speed_var = tk.StringVar(value="0")
        self.rpm_var = tk.StringVar(value="0")
        ttk.Label(frm, text="Current speed (mph):").grid(column=0, row=1, sticky="w")
        ttk.Label(frm, textvariable=self.speed_var, width=10).grid(column=1, row=1)

        ttk.Label(frm, text="Current RPM:").grid(column=0, row=2, sticky="w")
        ttk.Label(frm, textvariable=self.rpm_var, width=10).grid(column=1, row=2)

        # Injection controls
        ttk.Separator(frm).grid(column=0, row=3, columnspan=3, sticky="ew", pady=6)

        ttk.Label(frm, text="Inject spoofed telemetry").grid(column=0, row=4, sticky="w")
        self.inject_speed = tk.DoubleVar(value=200.0)
        self.inject_rpm = tk.IntVar(value=8000)
        ttk.Label(frm, text="Speed:").grid(column=0, row=5, sticky="w")
        ttk.Entry(frm, textvariable=self.inject_speed, width=10).grid(column=1, row=5)
        ttk.Label(frm, text="RPM:").grid(column=0, row=6, sticky="w")
        ttk.Entry(frm, textvariable=self.inject_rpm, width=10).grid(column=1, row=6)
        ttk.Button(frm, text="Inject Spoof", command=self.on_inject).grid(column=0, row=7, pady=8)

        ttk.Button(frm, text="Stop Simulation & Exit", command=self.on_quit).grid(column=1, row=7, pady=8)

    def on_inject(self):
        s = self.inject_speed.get()
        r = self.inject_rpm.get()
        inject_spoofed_telemetry(s, r)

    def update_ui_loop(self):
        # Poll shared_state and update labels
        with state_lock:
            drv = shared_state.get("driver_device_01", {})
            sp = drv.get("speed_mph", "—")
            rp = drv.get("rpm", "—")
        self.speed_var.set(str(sp))
        self.rpm_var.set(str(rp))
        # schedule next poll
        self.root.after(200, self.update_ui_loop)

    def on_quit(self):
        stop_async_thread()
        # give the async thread a moment to finish
        self.root.after(200, self.root.quit)


def main():
    th = start_async_thread()
    root = tk.Tk()
    app = SimpleGUI(root)
    try:
        root.mainloop()
    finally:
        # ensure proper shutdown
        stop_async_thread()
        th.join(timeout=1.0)


if __name__ == "__main__":
    main()
