"""Simplified LVGL based application for LabCobot."""

from __future__ import annotations

import logging
import math
import queue
import threading
import time
import types
from collections.abc import Mapping

import lvgl as lv

try:
    from .petri_workflow import ProcessRunTracker, error_guidance
except ImportError:
    from petri_workflow import ProcessRunTracker, error_guidance

LOGGER = logging.getLogger("labcobot.ui")


class MainApp:
    """Basic application structure using LVGL widgets."""

    def __init__(self, *, workcell_services=None) -> None:
        lv.init()
        self.workcell_services = workcell_services
        self.dialog = None
        self.play_sounds = False
        self.root = types.SimpleNamespace(current="main")
        self._action_in_progress = False
        self._arm_confirmation_deadline = 0.0
        self._recovery_confirmation_deadline = 0.0
        self.process_tracker = ProcessRunTracker()
        self.status_label = None
        self.service_label = None
        self.progress_label = None
        self._status_timer = None
        self._last_error_code = ""
        self._background_events = queue.SimpleQueue()
        self._process_thread = None
        self._recovery_thread = None

        self.main_screen = lv.obj()
        self.progress_screen = lv.obj()
        self.outbox_screen = lv.obj()
        self.registration_screen = lv.obj()

        self._build_ui()
        lv.scr_load(self.main_screen)

    # ------------------------------------------------------------------ helpers
    def _button(self, parent, text, x, y, cb):
        btn = lv.btn(parent)
        btn.set_pos(x, y)
        if hasattr(btn, "set_size"):
            # A minimum 48 px target is comfortable on the Pi touchscreen.
            btn.set_size(220, 48)
        lbl = lv.label(btn)
        lbl.set_text(text)
        btn.add_event_cb(
            lambda evt: self._run_ui_callback(text, cb),
            lv.EVENT.CLICKED,
            None,
        )
        return btn

    def _set_status(self, text: str) -> None:
        if self.status_label is not None:
            self.status_label.set_text(text)

    def _run_ui_callback(self, action_name: str, callback) -> None:
        """Keep callback failures and repeated taps from breaking the UI loop."""

        if self._action_in_progress:
            LOGGER.info("Ignoring repeated UI action while busy: %s", action_name)
            return

        self._action_in_progress = True
        self._set_status(f"{action_name}…")
        try:
            result = callback()
        except Exception:
            LOGGER.exception("UI action failed: %s", action_name)
            try:
                self.show_error_dialog(
                    f"{action_name} failed. Check the device log.",
                    "E_UI_ACTION",
                )
            except Exception:
                LOGGER.exception("Could not display UI error status")
        else:
            self._set_status(str(result) if result else "Ready")
        finally:
            self._action_in_progress = False
            self.refresh_service_status()

    def _event_queue(self):
        events = getattr(self, "_background_events", None)
        if events is None:
            events = queue.SimpleQueue()
            self._background_events = events
        return events

    def _post_background_event(
        self,
        kind: str,
        message: str,
        error_code: str = "",
    ) -> None:
        self._event_queue().put(
            (str(kind), str(message)[:400], str(error_code)[:80])
        )

    def _drain_background_events(self) -> None:
        """Apply background results on the LVGL timer/callback thread only."""

        events = self._event_queue()
        while True:
            try:
                kind, message, error_code = events.get_nowait()
            except queue.Empty:
                return
            if kind == "error":
                self._last_error_code = error_code or "E_UI_ACTION"
                self._set_status(
                    "{} ({})".format(message, self._last_error_code)
                )
            else:
                if kind == "recovered":
                    self._last_error_code = ""
                self._set_status(message)

    @staticmethod
    def _thread_is_running(thread) -> bool:
        return thread is not None and thread.is_alive()

    def _switch(self, name: str) -> None:
        if name not in {"main", "progress", "outbox", "registration"}:
            raise ValueError(f"Unknown screen: {name}")
        self.root.current = name
        screen = getattr(self, f"{name}_screen")
        lv.scr_load(screen)

    def _build_ui(self) -> None:
        title = lv.label(self.main_screen)
        title.set_text("LabCobot")
        if hasattr(title, "set_pos"):
            title.set_pos(10, 8)

        self._button(self.main_screen, "Start", 10, 48, self.start_process)
        self._button(self.main_screen, "Set ID", 10, 102, self.set_petri_dish_id)
        self._button(
            self.main_screen,
            "Registration",
            10,
            156,
            self.go_to_registration,
        )
        self._button(
            self.main_screen,
            "Outbox",
            10,
            210,
            lambda: self._switch("outbox"),
        )
        self._button(
            self.main_screen,
            "Check Updates",
            10,
            264,
            self.check_for_updates,
        )
        self._button(
            self.main_screen,
            "ARM imitation",
            10,
            318,
            self.arm_imitation,
        )
        self._button(
            self.main_screen,
            "STOP ARM",
            10,
            372,
            self.stop_motion,
        )

        self.status_label = lv.label(self.main_screen)
        self.status_label.set_text("Ready")
        if hasattr(self.status_label, "set_pos"):
            self.status_label.set_pos(10, 430)

        self.service_label = lv.label(self.main_screen)
        self.service_label.set_text(self._service_summary())
        if hasattr(self.service_label, "set_pos"):
            self.service_label.set_pos(260, 48)

        timer_create = getattr(lv, "timer_create", None)
        if callable(timer_create):
            self._status_timer = timer_create(
                lambda _timer: self.refresh_runtime_status(),
                1000,
                None,
            )

        for name in ["progress", "outbox", "registration"]:
            self._button(
                getattr(self, f"{name}_screen"),
                "Back",
                10,
                10,
                lambda n=name: self._switch("main"),
            )

        self.progress_label = lv.label(self.progress_screen)
        self.progress_label.set_text("No process running")
        if hasattr(self.progress_label, "set_pos"):
            self.progress_label.set_pos(10, 70)
        self._button(
            self.progress_screen,
            "Cancel process",
            10,
            130,
            self.cancel_process,
        )
        self._button(
            self.main_screen,
            "Error help",
            260,
            318,
            self.show_error_help,
        )
        self._button(
            self.main_screen,
            "Reviewed recovery",
            260,
            372,
            self.reset_after_error,
        )

    # ------------------------------------------------------------------ callbacks
    def start_process(self) -> str:
        services = getattr(self, "workcell_services", None)
        configured = getattr(
            services,
            "petri_process_configured",
            callable(getattr(services, "start_petri_process", None)),
        )
        if services is not None and not configured:
            raise RuntimeError("Petri process controller is not commissioned")
        starter = getattr(
            services,
            "start_petri_process",
            None,
        )
        if not callable(starter):
            raise RuntimeError("Petri process controller is not commissioned")
        if self._thread_is_running(getattr(self, "_process_thread", None)):
            raise RuntimeError("Petri process is already running")
        estimate_reader = getattr(
            services,
            "petri_process_estimate",
            None,
        )
        estimate = {}
        if callable(estimate_reader):
            estimate = dict(estimate_reader() or {})
        tracker = self._process_tracker()
        tracker.start(
            int(estimate.get("total_steps", 4)),
            estimated_seconds=float(
                estimate.get("estimated_seconds", 60.0)
            ),
        )
        self._switch("progress")
        self.refresh_progress_status()
        worker = threading.Thread(
            target=self._run_petri_process,
            args=(starter,),
            name="labcobot-petri-process",
            daemon=True,
        )
        self._process_thread = worker
        worker.start()
        return "Petri process started"

    def _run_petri_process(self, starter) -> None:
        try:
            result = starter(progress_callback=self.update_process_progress)
        except Exception as exc:
            LOGGER.exception("Petri process failed")
            snapshot = self._process_tracker().snapshot()
            if snapshot.state == "running":
                self._process_tracker().fail(
                    "E_PROCESS",
                    "Petri process failed: {}".format(type(exc).__name__),
                )
                self._post_background_event(
                    "error",
                    "Petri process failed; inspect the device log",
                    "E_PROCESS",
                )
            return

        snapshot = self._process_tracker().snapshot()
        if snapshot.state == "running":
            message = str(result or "Process completed")
            self._process_tracker().complete(message)
            self._post_background_event("status", message)

    def go_to_registration(self) -> None:
        self._switch("registration")

    def set_petri_dish_id(self) -> None:
        print("Set petri dish ID")

    def close_dialog(self, *args) -> None:
        if self.dialog is not None and hasattr(self.dialog, "dismiss"):
            try:
                self.dialog.dismiss()
            except Exception:
                LOGGER.debug("Dialog was already closed", exc_info=True)
        self.dialog = None
        self.root.current = "main"
        if hasattr(self, "main_screen"):
            lv.scr_load(self.main_screen)

    def cancel_process(self) -> str:
        tracker = self._process_tracker()
        tracker.cancel()
        services = getattr(self, "workcell_services", None)
        cancel = getattr(services, "cancel_petri_process", None)
        cancel_result = ""
        if callable(cancel):
            cancel_result = str(cancel())
        stop = getattr(services, "emergency_stop_motion", None)
        stop_result = ""
        if callable(stop):
            stop_result = str(stop())
        if self.play_sounds:
            try:
                self.progress_music.stop()
                self.error_sound.play()
            except Exception:
                LOGGER.warning("Could not play cancellation sound", exc_info=True)
        self.close_dialog()
        return (
            stop_result
            or cancel_result
            or "Process cancelled; motion locked"
        )

    def send_python_code(self, code_text: str) -> None:
        """Reject the retired unauthenticated remote-code execution workflow."""

        raise RuntimeError(
            "Remote Python code upload is disabled; use signed OTA releases"
        )

    def show_error_dialog(self, text: str, code: str) -> None:
        message = f"{text} ({code})"
        self._last_error_code = str(code)
        self._process_tracker().fail(code, text)
        self._set_status(message)
        LOGGER.error(message)

    def show_error_help(self) -> str:
        code = getattr(self, "_last_error_code", "") or "E_UI_ACTION"
        guidance = error_guidance(code)
        self._set_status(guidance)
        return guidance

    def reset_after_error(self) -> str:
        """Require a second operator tap before commissioned recovery motion."""

        snapshot = self._process_tracker().snapshot()
        if snapshot.state != "fault" or not snapshot.recovery_required:
            raise RuntimeError("No inspected process fault requires recovery")
        services = getattr(self, "workcell_services", None)
        recover = getattr(services, "recover_to_home", None)
        if not callable(recover):
            raise RuntimeError(
                "Reviewed return-to-home recovery is not commissioned"
            )
        now = time.monotonic()
        deadline = getattr(self, "_recovery_confirmation_deadline", 0.0)
        if now > deadline:
            self._recovery_confirmation_deadline = now + 5.0
            return "Confirm cleared workspace; tap recovery again within 5 seconds"
        if self._thread_is_running(getattr(self, "_recovery_thread", None)):
            raise RuntimeError("Reviewed recovery is already running")
        self._recovery_confirmation_deadline = 0.0
        worker = threading.Thread(
            target=self._run_reviewed_recovery,
            args=(recover,),
            name="labcobot-reviewed-recovery",
            daemon=True,
        )
        self._recovery_thread = worker
        worker.start()
        return "Reviewed recovery started; STOP remains available"

    def _run_reviewed_recovery(self, recover) -> None:
        try:
            result = str(recover())
        except Exception as exc:
            LOGGER.exception("Reviewed recovery failed")
            self._post_background_event(
                "error",
                "Reviewed recovery failed: {}".format(type(exc).__name__),
                "E_MOTION",
            )
            return
        snapshot = self._process_tracker().snapshot()
        if snapshot.state == "fault":
            self._process_tracker().mark_recovered()
        self._post_background_event(
            "recovered",
            result or "Home reached; motion remains LOCKED",
        )

    def update_process_progress(self, message: str = "") -> None:
        tracker = self._process_tracker()
        if tracker.snapshot().state == "running":
            tracker.advance(message)

    def refresh_progress_status(self) -> None:
        label = getattr(self, "progress_label", None)
        if label is None:
            return
        snapshot = self._process_tracker().snapshot()
        eta = (
            "calculating"
            if snapshot.eta_seconds is None
            else "{} s".format(int(round(snapshot.eta_seconds)))
        )
        label.set_text(
            "{}\nStep {} / {}\n{:.0f}%\nETA: {}\n{}".format(
                snapshot.state.upper(),
                snapshot.completed_steps,
                snapshot.total_steps,
                snapshot.progress_percent,
                eta,
                snapshot.message,
            )
        )

    def _process_tracker(self) -> ProcessRunTracker:
        tracker = getattr(self, "process_tracker", None)
        if tracker is None:
            tracker = ProcessRunTracker()
            self.process_tracker = tracker
        return tracker

    def check_for_updates(self, *args) -> str:
        if (
            self.workcell_services is not None
            and self.workcell_services.ota_configured
        ):
            if not self.workcell_services.request_update_check():
                return "Secure update check already running"
            return "Secure update check started"
        raise RuntimeError(
            "Secure OTA is not configured; legacy unsigned updates are disabled"
        )

    def arm_imitation(self) -> str:
        """Require a deliberate second tap before enabling any robot motion."""

        services = self.workcell_services
        if services is None or not services.motion_live_capable:
            self._arm_confirmation_deadline = 0.0
            raise RuntimeError(
                "Live imitation is not commissioned; dry-run remains active"
            )
        now = time.monotonic()
        if now > self._arm_confirmation_deadline:
            self._arm_confirmation_deadline = now + 5.0
            return "Confirm: tap ARM imitation again within 5 seconds"
        self._arm_confirmation_deadline = 0.0
        return services.arm_motion()

    def stop_motion(self) -> str:
        """Immediately close runtime gates and request a controller halt."""

        self._arm_confirmation_deadline = 0.0
        self._recovery_confirmation_deadline = 0.0
        if self.workcell_services is None:
            return "Motion LOCKED"
        return self.workcell_services.emergency_stop_motion()

    def refresh_service_status(self) -> None:
        """Refresh operational state without allowing diagnostics to break UI."""

        label = getattr(self, "service_label", None)
        if label is None:
            return
        try:
            label.set_text(self._service_summary())
        except Exception:
            LOGGER.warning("Could not refresh workcell status", exc_info=True)

    def refresh_runtime_status(self) -> None:
        self._drain_background_events()
        self.refresh_service_status()
        try:
            self.refresh_progress_status()
        except Exception:
            LOGGER.warning("Could not refresh process status", exc_info=True)

    def _service_summary(self) -> str:
        settings = getattr(self.workcell_services, "settings", None)
        if settings is None:
            return "Workcell services: development mode\nMotion: LOCKED"
        device = settings.device_uid or "not provisioned"
        runtime_status = {}
        status_reader = getattr(
            self.workcell_services,
            "runtime_status",
            None,
        )
        if callable(status_reader):
            try:
                runtime_status = dict(status_reader())
            except Exception:
                LOGGER.warning("Could not read workcell status", exc_info=True)
        camera_health = runtime_status.get("cameras") or {}
        if camera_health:
            camera_summary = ", ".join(
                "{}={}".format(role, values.get("state", "unknown"))
                for role, values in sorted(camera_health.items())
            )
        else:
            camera_summary = (
                "configured" if settings.cameras_enabled else "off"
            )
        datahub_status = runtime_status.get("datahub") or {}
        if datahub_status.get("configured"):
            datahub_summary = "queued {}, rejected {}".format(
                datahub_status.get("pending", "?"),
                datahub_status.get("dead_letters", "?"),
            )
        else:
            datahub_summary = (
                "configured" if settings.datahub_configured else "off"
            )
        runtime_summary = str(runtime_status.get("status") or "starting")
        registry_summary = str(
            (runtime_status.get("registry") or {}).get("status")
            or (
                "configured"
                if getattr(settings, "registry_configured", False)
                else "off"
            )
        )
        motion_status = {}
        motion_reader = getattr(
            self.workcell_services,
            "motion_status",
            None,
        )
        if callable(motion_reader):
            try:
                motion_status = dict(motion_reader() or {})
            except Exception:
                LOGGER.warning("Could not read motion status", exc_info=True)

        lines = [
            "Robotic stand: {}".format(device),
            "Runtime: {}".format(runtime_summary),
            "Registry: {}".format(registry_summary),
            "Tool + room cameras: {}".format(camera_summary),
            "DataHub: {}".format(datahub_summary),
            "Signed OTA: {}".format(
                "configured" if settings.ota_configured else "off"
            ),
            "Compute offload: {}".format(
                "configured" if settings.offload_configured else "off"
            ),
            "Tool-camera calibration: {}".format(
                "active"
                if getattr(
                    self.workcell_services,
                    "eye_in_hand_configured",
                    False,
                )
                else "off"
            ),
            "Valve discovery/imitation: {}".format(
                (
                    "automatic; live is operator-gated"
                    if getattr(
                        self.workcell_services,
                        "motion_live_capable",
                        False,
                    )
                    else "automatic dry-run"
                )
                if getattr(
                    self.workcell_services,
                    "dynamic_valve_configured",
                    False,
                )
                else "off"
            ),
            "Motion: {}".format(motion_status.get("state", "LOCKED")),
        ]

        target_review = motion_status.get("last_target_review")
        if isinstance(target_review, Mapping):
            target_id = target_review.get("target_id")
            if target_id:
                lines.append("Last valve: {}".format(str(target_id)[:120]))

            mapped_xyz = target_review.get("mapped_xyz_mm")
            try:
                mapped_values = tuple(float(value) for value in mapped_xyz)
            except (TypeError, ValueError, OverflowError):
                mapped_values = ()
            if (
                len(mapped_values) == 3
                and all(math.isfinite(value) for value in mapped_values)
            ):
                lines.append(
                    "Mapped XYZ mm: {:.1f}, {:.1f}, {:.1f}".format(
                        *mapped_values
                    )
                )

            confidence = target_review.get("confidence")
            try:
                confidence_value = float(confidence)
            except (TypeError, ValueError, OverflowError):
                confidence_value = -1.0
            if (
                math.isfinite(confidence_value)
                and 0.0 <= confidence_value <= 1.0
            ):
                lines.append(
                    "Confidence: {:.0%}".format(confidence_value)
                )

            if "remote_confirmed" in target_review:
                lines.append(
                    "Remote confirmation: {}".format(
                        "yes"
                        if bool(target_review.get("remote_confirmed"))
                        else "no"
                    )
                )

            cartesian_count = target_review.get("cartesian_waypoints")
            joint_count = target_review.get("joint_waypoints")
            if cartesian_count is not None or joint_count is not None:
                lines.append(
                    "Plan waypoints: Cartesian {}, joint {}".format(
                        cartesian_count if cartesian_count is not None else "?",
                        joint_count if joint_count is not None else "?",
                    )
                )

        lines.append(
            "STOP button is software-only; keep physical E-stop accessible"
        )
        return "\n".join(lines)


if __name__ == "__main__":
    # Keep direct development launches on the same integration path as the
    # systemd unit.  Bypassing runtime.main() would omit cameras, telemetry,
    # signed OTA, and remote-compute configuration.
    from runtime import main

    raise SystemExit(main())
