use std::time::Duration;

use crate::{
    ActuatorConfig, ActuatorFeedback, ActuatorHealth, ActuatorProtocol, ActuatorSnapshot, CanBus,
    CanFrame, CanFrameRecord, CanMonitor, Result, sample_can_monitor,
};
use scrin::effects::{EffectKind, EffectPlayer};
use scrin::widgets::Widget;
use scrin::{Buffer, Color, Frame, Rect};
use scrin_widgets::{AislingPalette, List};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TuiMode {
    View,
    ControlArmed,
}

impl TuiMode {
    pub const fn label(self) -> &'static str {
        match self {
            Self::View => "VIEW",
            Self::ControlArmed => "CONTROL ARMED",
        }
    }

    pub const fn is_armed(self) -> bool {
        matches!(self, Self::ControlArmed)
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TuiTarget {
    pub id: u8,
    pub protocol: ActuatorProtocol,
}

impl TuiTarget {
    pub const fn from_config(config: &ActuatorConfig) -> Self {
        Self {
            id: config.id,
            protocol: config.protocol,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TuiCommand {
    SelectNext,
    SelectPrevious,
    RefreshSelected,
    ArmControl,
    DisarmControl,
    ToggleControlArm,
    EnableSelected,
    StopSelected,
    ZeroSelected,
    EmergencyStopAll,
    Quit,
}

impl TuiCommand {
    pub const fn from_key(key: char) -> Option<Self> {
        match key {
            'j' | 'n' => Some(Self::SelectNext),
            'k' | 'p' => Some(Self::SelectPrevious),
            'r' => Some(Self::RefreshSelected),
            'a' => Some(Self::ToggleControlArm),
            'e' => Some(Self::EnableSelected),
            's' => Some(Self::StopSelected),
            'z' => Some(Self::ZeroSelected),
            '!' => Some(Self::EmergencyStopAll),
            'q' => Some(Self::Quit),
            _ => None,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TuiControlIntent {
    None,
    Refresh(TuiTarget),
    Enable(TuiTarget),
    Stop(TuiTarget),
    ZeroPosition(TuiTarget),
    EmergencyStopAll,
    Quit,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum CanMonitorCommand {
    SelectNext,
    SelectPrevious,
    Pause,
    Resume,
    TogglePause,
    Clear,
    Quit,
}

impl CanMonitorCommand {
    pub const fn from_key(key: char) -> Option<Self> {
        match key {
            'j' | 'n' => Some(Self::SelectNext),
            'k' | 'p' => Some(Self::SelectPrevious),
            ' ' => Some(Self::TogglePause),
            'c' => Some(Self::Clear),
            'q' => Some(Self::Quit),
            _ => None,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TuiActuatorAnalytics {
    pub samples: u64,
    pub fault_samples: u64,
    pub first_tick: Option<u64>,
    pub last_tick: Option<u64>,
    pub min_position: Option<f64>,
    pub max_position: Option<f64>,
    pub peak_abs_effort: Option<f64>,
}

impl TuiActuatorAnalytics {
    pub const fn empty() -> Self {
        Self {
            samples: 0,
            fault_samples: 0,
            first_tick: None,
            last_tick: None,
            min_position: None,
            max_position: None,
            peak_abs_effort: None,
        }
    }

    pub fn observe(&mut self, snapshot: &ActuatorSnapshot, tick: u64) {
        if snapshot.feedback.is_none() {
            return;
        }

        self.samples = self.samples.saturating_add(1);
        self.first_tick.get_or_insert(tick);
        self.last_tick = Some(tick);

        if snapshot.health() == ActuatorHealth::Fault {
            self.fault_samples = self.fault_samples.saturating_add(1);
        }

        if let Some(position) = snapshot_position(snapshot) {
            self.min_position = Some(match self.min_position {
                Some(current) => current.min(position),
                None => position,
            });
            self.max_position = Some(match self.max_position {
                Some(current) => current.max(position),
                None => position,
            });
        }

        if let Some(effort) = snapshot_effort(snapshot) {
            self.peak_abs_effort = Some(match self.peak_abs_effort {
                Some(current) => current.max(effort.abs()),
                None => effort.abs(),
            });
        }
    }

    pub fn age_ticks(self, current_tick: u64) -> Option<u64> {
        self.last_tick.map(|tick| current_tick.saturating_sub(tick))
    }

    pub fn position_span(self) -> Option<f64> {
        Some(self.max_position? - self.min_position?)
    }
}

impl Default for TuiActuatorAnalytics {
    fn default() -> Self {
        Self::empty()
    }
}

#[derive(Debug, Clone)]
pub struct TuiCanMonitor {
    monitor: CanMonitor,
    selected_index: usize,
    tick: u64,
    paused: bool,
    last_message: Option<String>,
    palette: AislingPalette,
}

impl TuiCanMonitor {
    pub fn new(monitor: CanMonitor) -> Self {
        Self {
            monitor,
            selected_index: 0,
            tick: 0,
            paused: false,
            last_message: None,
            palette: AislingPalette::cypherpunk(),
        }
    }

    pub fn sample() -> Result<Self> {
        Ok(Self::new(sample_can_monitor()?))
    }

    pub fn with_palette(mut self, palette: AislingPalette) -> Self {
        self.palette = palette;
        self
    }

    pub const fn selected_index(&self) -> usize {
        self.selected_index
    }

    pub const fn is_paused(&self) -> bool {
        self.paused
    }

    pub fn monitor(&self) -> &CanMonitor {
        &self.monitor
    }

    pub fn monitor_mut(&mut self) -> &mut CanMonitor {
        &mut self.monitor
    }

    pub fn last_message(&self) -> Option<&str> {
        self.last_message.as_deref()
    }

    pub fn observe(&mut self, frame: CanFrame) {
        self.monitor.observe(frame);
        self.clamp_selection();
    }

    pub fn poll_available<B>(
        &mut self,
        bus: &mut B,
        timeout: Duration,
        max_frames: usize,
    ) -> Result<usize>
    where
        B: CanBus,
    {
        if self.paused {
            return Ok(0);
        }

        let received = self.monitor.poll_available(bus, timeout, max_frames)?;
        if received > 0 {
            self.last_message = Some(format!("received {received} frame(s)"));
        }
        self.clamp_selection();
        Ok(received)
    }

    pub fn apply_command(&mut self, command: CanMonitorCommand) -> bool {
        match command {
            CanMonitorCommand::SelectNext => {
                self.select_next();
                false
            }
            CanMonitorCommand::SelectPrevious => {
                self.select_previous();
                false
            }
            CanMonitorCommand::Pause => {
                self.paused = true;
                self.last_message = Some("CAN capture paused".to_owned());
                false
            }
            CanMonitorCommand::Resume => {
                self.paused = false;
                self.last_message = Some("CAN capture resumed".to_owned());
                false
            }
            CanMonitorCommand::TogglePause => {
                if self.paused {
                    self.apply_command(CanMonitorCommand::Resume)
                } else {
                    self.apply_command(CanMonitorCommand::Pause)
                }
            }
            CanMonitorCommand::Clear => {
                self.monitor.clear();
                self.selected_index = 0;
                self.last_message = Some("CAN log cleared".to_owned());
                false
            }
            CanMonitorCommand::Quit => true,
        }
    }

    pub fn advance_tick(&mut self) {
        self.tick = self.tick.wrapping_add(1);
    }

    pub fn select_next(&mut self) {
        let count = self.monitor.records().len();
        if count > 0 {
            self.selected_index = (self.selected_index + 1) % count;
        }
    }

    pub fn select_previous(&mut self) {
        let count = self.monitor.records().len();
        if count == 0 {
            return;
        }
        self.selected_index = self.selected_index.checked_sub(1).unwrap_or(count - 1);
    }

    pub fn selected_record(&self) -> Option<&CanFrameRecord> {
        self.monitor.records().iter().rev().nth(self.selected_index)
    }

    pub fn row_strings(&self) -> Vec<String> {
        let rows = self.monitor.rows();
        if rows.is_empty() {
            return vec!["waiting for CAN frames".to_owned()];
        }

        rows.into_iter()
            .enumerate()
            .map(|(index, row)| {
                let marker = if index == self.selected_index {
                    ">"
                } else {
                    " "
                };
                format!("{marker} {row}")
            })
            .collect()
    }

    pub fn render_to_buffer(&self, buffer: &mut Buffer) {
        let width = buffer.width().min(usize::from(u16::MAX)) as u16;
        let height = buffer.height().min(usize::from(u16::MAX)) as u16;
        self.render(buffer, Rect::new(0, 0, width, height));
    }

    pub fn render_frame(&self, frame: &mut Frame<'_>) {
        let area = frame.area();
        self.render(frame.buffer(), area);
    }

    pub fn render_plain(&self, width: usize, height: usize) -> String {
        let mut buffer = Buffer::new(width, height);
        self.render_to_buffer(&mut buffer);
        buffer.to_plain_string()
    }

    pub fn render(&self, buffer: &mut Buffer, area: Rect) {
        if area.is_empty() {
            return;
        }

        buffer.fill(area, ' ', self.palette.low, Some(Color::BLACK));

        let header_height = if area.height >= 16 { 7 } else { 5 }.min(area.height);
        let footer_height = if area.height >= 9 { 3 } else { 0 };
        let content_height = area
            .height
            .saturating_sub(header_height)
            .saturating_sub(footer_height);

        let header = Rect::new(area.x, area.y, area.width, header_height);
        let content = Rect::new(
            area.x,
            area.y.saturating_add(header_height),
            area.width,
            content_height,
        );
        let footer = Rect::new(
            area.x,
            area.bottom().saturating_sub(footer_height),
            area.width,
            footer_height,
        );

        self.render_can_header(buffer, header);

        if !content.is_empty() {
            if content.width >= 100 && content.height >= 8 {
                let list_width = content.width.saturating_mul(64) / 100;
                let list_area = Rect::new(content.x, content.y, list_width, content.height);
                let detail_area = Rect::new(
                    content.x.saturating_add(list_width),
                    content.y,
                    content.width.saturating_sub(list_width),
                    content.height,
                );
                self.render_can_list(buffer, list_area);
                self.render_can_detail(buffer, detail_area);
            } else {
                let list_height = (content.height.saturating_mul(2) / 3).max(3);
                let list_height = list_height.min(content.height);
                let list_area = Rect::new(content.x, content.y, content.width, list_height);
                let detail_area = Rect::new(
                    content.x,
                    content.y.saturating_add(list_height),
                    content.width,
                    content.height.saturating_sub(list_height),
                );
                self.render_can_list(buffer, list_area);
                self.render_can_detail(buffer, detail_area);
            }
        }

        self.render_can_footer(buffer, footer);
    }

    fn clamp_selection(&mut self) {
        let count = self.monitor.records().len();
        if count == 0 {
            self.selected_index = 0;
        } else if self.selected_index >= count {
            self.selected_index = count - 1;
        }
    }

    fn render_can_header(&self, buffer: &mut Buffer, area: Rect) {
        if area.is_empty() {
            return;
        }

        let title_right = if self.paused { "PAUSED" } else { "LIVE" };
        let block = self
            .palette
            .block("CAN monitor")
            .with_title_right(title_right);
        let inner = block.inner(area);
        block.render(buffer, area);

        let stats = self.monitor.stats();
        let summary = format!(
            "frames={} retained={} dropped={} std={} ext={} bytes={}",
            stats.frames_seen,
            stats.frames_retained,
            stats.frames_dropped,
            stats.standard_frames,
            stats.extended_frames,
            stats.bytes_seen
        );
        draw_line(buffer, inner, 0, &summary, self.palette.high);

        if inner.height > 2 {
            let effect_area = Rect::new(
                inner.x,
                inner.y.saturating_add(1),
                inner.width,
                inner.height.saturating_sub(2),
            );
            self.render_signal_effect(buffer, effect_area);
        }

        if let Some(message) = &self.last_message {
            draw_line(
                buffer,
                inner,
                inner.height.saturating_sub(1),
                message,
                self.palette.pulse,
            );
        }
    }

    fn render_signal_effect(&self, buffer: &mut Buffer, area: Rect) {
        if area.is_empty() {
            return;
        }

        let mut effect = EffectPlayer::new(EffectKind::Matrix, "CAN BUS DATA")
            .with_size(usize::from(area.width), usize::from(area.height))
            .with_duration(96);
        for _ in 0..(self.tick % 96) {
            effect.advance();
        }
        effect.render_to_buffer(buffer, area);
    }

    fn render_can_list(&self, buffer: &mut Buffer, area: Rect) {
        if area.is_empty() {
            return;
        }

        let selected = (!self.monitor.records().is_empty()).then_some(self.selected_index);
        let list = List::new()
            .items(self.row_strings())
            .selected(selected)
            .tick(self.tick)
            .palette(self.palette)
            .block(self.palette.block("Frames"));
        list.render(buffer, area);
    }

    fn render_can_detail(&self, buffer: &mut Buffer, area: Rect) {
        if area.is_empty() {
            return;
        }

        let block = self.palette.block("Frame data");
        let inner = block.inner(area);
        block.render(buffer, area);

        for (index, line) in self.detail_lines().iter().enumerate() {
            draw_line(buffer, inner, index as u16, line, self.palette.high);
        }
    }

    fn render_can_footer(&self, buffer: &mut Buffer, area: Rect) {
        if area.is_empty() {
            return;
        }

        let block = self.palette.block("Keys");
        let inner = block.inner(area);
        block.render(buffer, area);
        draw_line(
            buffer,
            inner,
            0,
            "j/k select, space pause/resume, c clear, q quit",
            self.palette.pulse,
        );
    }

    fn detail_lines(&self) -> Vec<String> {
        let Some(record) = self.selected_record() else {
            return vec!["no CAN frame selected".to_owned()];
        };
        let formatted = record.formatted();

        vec![
            format!("sequence: {}", record.sequence),
            format!("elapsed: {:.3}s", record.elapsed.as_secs_f64()),
            format!("id: {}", formatted.id),
            format!("len: {}", formatted.len),
            format!("class: {}", formatted.class),
            format!("hex: {}", formatted.data),
            format!("ascii: {}", formatted.ascii),
        ]
    }
}

#[derive(Debug, Clone)]
pub struct TuiDashboard {
    snapshots: Vec<ActuatorSnapshot>,
    analytics: Vec<TuiActuatorAnalytics>,
    selected_index: usize,
    mode: TuiMode,
    controls_enabled: bool,
    tick: u64,
    last_message: Option<String>,
    palette: AislingPalette,
}

impl TuiDashboard {
    pub fn new(snapshots: Vec<ActuatorSnapshot>) -> Self {
        let mut dashboard = Self {
            analytics: Vec::with_capacity(snapshots.len()),
            snapshots,
            selected_index: 0,
            mode: TuiMode::View,
            controls_enabled: true,
            tick: 0,
            last_message: None,
            palette: AislingPalette::phosphor(),
        };
        dashboard.rebuild_analytics_from_current_snapshots();
        dashboard
    }

    pub fn with_palette(mut self, palette: AislingPalette) -> Self {
        self.palette = palette;
        self
    }

    pub const fn with_controls_enabled(mut self, enabled: bool) -> Self {
        self.controls_enabled = enabled;
        if !enabled {
            self.mode = TuiMode::View;
        }
        self
    }

    pub fn motor_count(&self) -> usize {
        self.snapshots.len()
    }

    pub const fn mode(&self) -> TuiMode {
        self.mode
    }

    pub const fn controls_enabled(&self) -> bool {
        self.controls_enabled
    }

    pub const fn selected_index(&self) -> usize {
        self.selected_index
    }

    pub fn snapshots(&self) -> &[ActuatorSnapshot] {
        &self.snapshots
    }

    pub fn analytics(&self) -> &[TuiActuatorAnalytics] {
        &self.analytics
    }

    pub fn selected_snapshot(&self) -> Option<&ActuatorSnapshot> {
        self.snapshots.get(self.selected_index)
    }

    pub fn selected_analytics(&self) -> Option<&TuiActuatorAnalytics> {
        self.analytics.get(self.selected_index)
    }

    pub fn last_message(&self) -> Option<&str> {
        self.last_message.as_deref()
    }

    pub fn advance_tick(&mut self) {
        self.tick = self.tick.wrapping_add(1);
    }

    pub fn set_snapshots(&mut self, snapshots: Vec<ActuatorSnapshot>) {
        let analytics = snapshots
            .iter()
            .map(|snapshot| {
                let mut analytics = self
                    .snapshot_index(snapshot)
                    .and_then(|index| self.analytics.get(index).copied())
                    .unwrap_or_default();
                analytics.observe(snapshot, self.tick);
                analytics
            })
            .collect();

        self.snapshots = snapshots;
        self.analytics = analytics;
        self.clamp_selection();
        self.lock_control_if_armed("control commands locked after actuator update");
    }

    pub fn upsert_snapshot(&mut self, snapshot: ActuatorSnapshot) {
        if let Some(index) = self.snapshot_index(&snapshot) {
            self.snapshots[index] = snapshot;
            self.analytics[index].observe(&self.snapshots[index], self.tick);
        } else {
            let mut analytics = TuiActuatorAnalytics::empty();
            analytics.observe(&snapshot, self.tick);
            self.snapshots.push(snapshot);
            self.analytics.push(analytics);
        }
        self.clamp_selection();
        self.lock_control_if_armed("control commands locked after actuator update");
    }

    pub fn select_next(&mut self) {
        self.lock_control_if_armed("control commands locked after navigation");
        if !self.snapshots.is_empty() {
            self.selected_index = (self.selected_index + 1) % self.snapshots.len();
        }
    }

    pub fn select_previous(&mut self) {
        self.lock_control_if_armed("control commands locked after navigation");
        if self.snapshots.is_empty() {
            return;
        }
        self.selected_index = self
            .selected_index
            .checked_sub(1)
            .unwrap_or(self.snapshots.len() - 1);
    }

    pub fn apply_command(&mut self, command: TuiCommand) -> TuiControlIntent {
        match command {
            TuiCommand::SelectNext => {
                self.select_next();
                TuiControlIntent::None
            }
            TuiCommand::SelectPrevious => {
                self.select_previous();
                TuiControlIntent::None
            }
            TuiCommand::RefreshSelected => self
                .selected_target()
                .map(TuiControlIntent::Refresh)
                .unwrap_or(TuiControlIntent::None),
            TuiCommand::ArmControl => {
                if !self.controls_enabled {
                    self.last_message =
                        Some("control commands disabled in read-only dashboard".to_owned());
                    return TuiControlIntent::None;
                }
                if self.snapshots.is_empty() {
                    self.last_message =
                        Some("cannot arm control; no actuators configured".to_owned());
                    return TuiControlIntent::None;
                }

                self.mode = TuiMode::ControlArmed;
                self.last_message = Some("control commands armed".to_owned());
                TuiControlIntent::None
            }
            TuiCommand::DisarmControl => {
                self.mode = TuiMode::View;
                self.last_message = Some("control commands locked".to_owned());
                TuiControlIntent::None
            }
            TuiCommand::ToggleControlArm => {
                if self.mode.is_armed() {
                    self.apply_command(TuiCommand::DisarmControl)
                } else {
                    self.apply_command(TuiCommand::ArmControl)
                }
            }
            TuiCommand::EnableSelected => self.armed_target_intent(TuiControlIntent::Enable),
            TuiCommand::StopSelected => self.armed_target_intent(TuiControlIntent::Stop),
            TuiCommand::ZeroSelected => self.armed_target_intent(TuiControlIntent::ZeroPosition),
            TuiCommand::EmergencyStopAll => {
                if !self.controls_enabled {
                    self.last_message =
                        Some("control commands disabled in read-only dashboard".to_owned());
                    return TuiControlIntent::None;
                }

                self.mode = TuiMode::View;
                self.last_message = Some("emergency stop requested for all actuators".to_owned());
                TuiControlIntent::EmergencyStopAll
            }
            TuiCommand::Quit => TuiControlIntent::Quit,
        }
    }

    pub fn row_strings(&self) -> Vec<String> {
        if self.snapshots.is_empty() {
            return vec!["no actuators configured".to_owned()];
        }

        self.snapshots
            .iter()
            .enumerate()
            .map(|(index, snapshot)| {
                let marker = if index == self.selected_index {
                    ">"
                } else {
                    " "
                };
                format!(
                    "{marker} {:<18} {:<14} {}",
                    snapshot.config.display_name(),
                    snapshot.config.protocol.family_name(),
                    snapshot.status_text()
                )
            })
            .collect()
    }

    pub fn render_to_buffer(&self, buffer: &mut Buffer) {
        let width = buffer.width().min(usize::from(u16::MAX)) as u16;
        let height = buffer.height().min(usize::from(u16::MAX)) as u16;
        self.render(buffer, Rect::new(0, 0, width, height));
    }

    pub fn render_frame(&self, frame: &mut Frame<'_>) {
        let area = frame.area();
        self.render(frame.buffer(), area);
    }

    pub fn render_plain(&self, width: usize, height: usize) -> String {
        let mut buffer = Buffer::new(width, height);
        self.render_to_buffer(&mut buffer);
        buffer.to_plain_string()
    }

    pub fn render(&self, buffer: &mut Buffer, area: Rect) {
        if area.is_empty() {
            return;
        }

        buffer.fill(area, ' ', self.palette.low, Some(Color::BLACK));

        let header_height = area.height.min(5);
        let footer_height = if area.height >= 9 { 3 } else { 0 };
        let content_height = area
            .height
            .saturating_sub(header_height)
            .saturating_sub(footer_height);

        let header = Rect::new(area.x, area.y, area.width, header_height);
        let content = Rect::new(
            area.x,
            area.y.saturating_add(header_height),
            area.width,
            content_height,
        );
        let footer = Rect::new(
            area.x,
            area.bottom().saturating_sub(footer_height),
            area.width,
            footer_height,
        );

        self.render_header(buffer, header);

        if !content.is_empty() {
            if content.width >= 112 && content.height >= 12 && self.snapshots.len() > 1 {
                self.render_pane_wall(buffer, content);
            } else if content.width >= 88 && content.height >= 8 {
                let list_width = content.width.saturating_mul(45) / 100;
                let detail_width = content.width.saturating_sub(list_width);
                let list_area = Rect::new(content.x, content.y, list_width, content.height);
                let detail_area = Rect::new(
                    content.x.saturating_add(list_width),
                    content.y,
                    detail_width,
                    content.height,
                );
                self.render_list(buffer, list_area);
                self.render_detail(buffer, detail_area);
            } else {
                let list_height = (content.height / 2).max(3);
                let list_height = list_height.min(content.height);
                let list_area = Rect::new(content.x, content.y, content.width, list_height);
                let detail_area = Rect::new(
                    content.x,
                    content.y.saturating_add(list_height),
                    content.width,
                    content.height.saturating_sub(list_height),
                );
                self.render_list(buffer, list_area);
                self.render_detail(buffer, detail_area);
            }
        }

        self.render_footer(buffer, footer);
    }

    fn render_pane_wall(&self, buffer: &mut Buffer, area: Rect) {
        if area.is_empty() {
            return;
        }
        if self.snapshots.is_empty() {
            draw_line(buffer, area, 0, "no actuators configured", self.palette.mid);
            return;
        }

        let columns = if area.width >= 150 {
            3
        } else if area.width >= 112 {
            2
        } else {
            1
        };
        let rows = self.snapshots.len().div_ceil(columns);
        let pane_width = area.width / columns as u16;
        let pane_height = (area.height / rows.max(1) as u16).max(6);

        for (index, snapshot) in self.snapshots.iter().enumerate() {
            let column = index % columns;
            let row = index / columns;
            let x = area
                .x
                .saturating_add(pane_width.saturating_mul(column as u16));
            let y = area
                .y
                .saturating_add(pane_height.saturating_mul(row as u16));
            if y >= area.bottom() {
                break;
            }
            let width = if column + 1 == columns {
                area.x.saturating_add(area.width).saturating_sub(x)
            } else {
                pane_width
            };
            let height = pane_height.min(area.bottom().saturating_sub(y));
            self.render_snapshot_pane(
                index,
                snapshot,
                index == self.selected_index,
                buffer,
                Rect::new(x, y, width, height),
            );
        }
    }

    fn render_snapshot_pane(
        &self,
        index: usize,
        snapshot: &ActuatorSnapshot,
        selected: bool,
        buffer: &mut Buffer,
        area: Rect,
    ) {
        if area.is_empty() {
            return;
        }

        let health = snapshot.health();
        let block_title = if selected { "Selected" } else { "Actuator" };
        let block = self
            .palette
            .block(block_title)
            .with_title_right(health.label());
        let inner = block.inner(area);
        block.render(buffer, area);

        draw_line(
            buffer,
            inner,
            0,
            &snapshot.config.display_name(),
            self.health_color(health),
        );
        draw_line(
            buffer,
            inner,
            1,
            snapshot.config.protocol.model_name(),
            self.palette.mid,
        );

        for (row, metric) in snapshot
            .metrics()
            .iter()
            .take(inner.height.saturating_sub(4) as usize)
            .enumerate()
        {
            let line = format!("{:<9} {:>10} {}", metric.label, metric.value, metric.unit);
            draw_line(
                buffer,
                inner,
                row as u16 + 2,
                &line,
                self.health_color(metric.health),
            );
        }

        if let Some(message) = &self.last_message {
            draw_line(
                buffer,
                inner,
                inner.height.saturating_sub(1),
                message,
                self.palette.pulse,
            );
        } else if let Some(analytics) = self.analytics.get(index) {
            if inner.height >= 4 {
                draw_line(
                    buffer,
                    inner,
                    inner.height.saturating_sub(2),
                    &self.analytics_summary_line(analytics),
                    self.palette.mid,
                );
                if let Some(line) = self.analytics_range_line(snapshot, analytics) {
                    draw_line(
                        buffer,
                        inner,
                        inner.height.saturating_sub(1),
                        &line,
                        self.palette.mid,
                    );
                }
            }
        }
    }

    fn selected_target(&self) -> Option<TuiTarget> {
        self.selected_snapshot()
            .map(|snapshot| TuiTarget::from_config(&snapshot.config))
    }

    fn armed_target_intent(
        &mut self,
        build: fn(TuiTarget) -> TuiControlIntent,
    ) -> TuiControlIntent {
        if !self.controls_enabled {
            self.last_message = Some("control commands disabled in read-only dashboard".to_owned());
            return TuiControlIntent::None;
        }

        if !self.mode.is_armed() {
            self.last_message = Some("control command blocked; press 'a' to arm".to_owned());
            return TuiControlIntent::None;
        }

        let Some(target) = self.selected_target() else {
            self.mode = TuiMode::View;
            self.last_message = Some("no actuator selected; control commands locked".to_owned());
            return TuiControlIntent::None;
        };

        self.mode = TuiMode::View;
        self.last_message = Some("control intent emitted; commands locked".to_owned());
        build(target)
    }

    fn lock_control_if_armed(&mut self, message: &str) {
        if self.mode.is_armed() {
            self.mode = TuiMode::View;
            self.last_message = Some(message.to_owned());
        }
    }

    fn clamp_selection(&mut self) {
        if self.snapshots.is_empty() {
            self.selected_index = 0;
        } else if self.selected_index >= self.snapshots.len() {
            self.selected_index = self.snapshots.len() - 1;
        }
    }

    fn snapshot_index(&self, snapshot: &ActuatorSnapshot) -> Option<usize> {
        self.snapshots.iter().position(|candidate| {
            candidate.config.id == snapshot.config.id
                && candidate.config.protocol == snapshot.config.protocol
        })
    }

    fn rebuild_analytics_from_current_snapshots(&mut self) {
        self.analytics = self
            .snapshots
            .iter()
            .map(|snapshot| {
                let mut analytics = TuiActuatorAnalytics::empty();
                analytics.observe(snapshot, self.tick);
                analytics
            })
            .collect();
    }

    fn render_header(&self, buffer: &mut Buffer, area: Rect) {
        if area.is_empty() {
            return;
        }

        let block = self
            .palette
            .block("kcan")
            .with_title_right(self.mode.label());
        let inner = block.inner(area);
        block.render(buffer, area);

        let selected = self
            .selected_snapshot()
            .map(|snapshot| snapshot.config.display_name())
            .unwrap_or_else(|| "none".to_owned());
        let summary = format!(
            "motors={} selected={} mode={}",
            self.motor_count(),
            selected,
            self.mode.label()
        );
        draw_line(buffer, inner, 0, &summary, self.palette.high);
        draw_line(
            buffer,
            inner,
            1,
            "safe view mode starts locked; arm before direct control intents",
            self.palette.mid,
        );
    }

    fn render_list(&self, buffer: &mut Buffer, area: Rect) {
        if area.is_empty() {
            return;
        }

        let selected = (!self.snapshots.is_empty()).then_some(self.selected_index);
        let list = List::new()
            .items(self.row_strings())
            .selected(selected)
            .tick(self.tick)
            .palette(self.palette)
            .block(self.palette.block("Actuators"));

        list.render(buffer, area);
    }

    fn render_detail(&self, buffer: &mut Buffer, area: Rect) {
        if area.is_empty() {
            return;
        }

        let block = self.palette.block("Selected");
        let inner = block.inner(area);
        block.render(buffer, area);

        let lines = self.detail_lines();
        for (index, line) in lines.iter().enumerate() {
            draw_line(buffer, inner, index as u16, line, self.palette.high);
        }
    }

    fn render_footer(&self, buffer: &mut Buffer, area: Rect) {
        if area.is_empty() {
            return;
        }

        let block = self.palette.block("Controls");
        let inner = block.inner(area);
        block.render(buffer, area);

        let controls = if !self.controls_enabled {
            "READ ONLY: j/k select, r refresh, q quit"
        } else if self.mode.is_armed() {
            "ARMED one-shot: e enable, s stop, z zero, a lock, ! emergency stop, q quit"
        } else {
            "j/k select, r refresh, a arm controls, ! emergency stop, q quit"
        };
        draw_line(buffer, inner, 0, controls, self.palette.pulse);
    }

    fn detail_lines(&self) -> Vec<String> {
        let mut lines = Vec::new();

        match self.selected_snapshot() {
            Some(snapshot) => {
                lines.push(format!("name: {}", snapshot.config.display_name()));
                lines.push(format!(
                    "protocol: {} / {}",
                    snapshot.config.protocol.family_name(),
                    snapshot.config.protocol.model_name()
                ));
                lines.push(format!("id: 0x{:02X}", snapshot.config.id));
                lines.push(format!("health: {}", snapshot.health().label()));
                lines.push(format!("status: {}", snapshot.status_text()));
                if let Some(analytics) = self.selected_analytics() {
                    lines.push(format!(
                        "analytics: {}",
                        self.analytics_summary_line(analytics)
                    ));
                    if let Some(line) = self.analytics_range_line(snapshot, analytics) {
                        lines.push(format!("analytics: {line}"));
                    }
                }
                for metric in snapshot.metrics() {
                    lines.push(format!(
                        "metric: {:<9} {:>10} {} [{}]",
                        metric.label,
                        metric.value,
                        metric.unit,
                        metric.health.label()
                    ));
                }
            }
            None => lines.push("no actuator selected".to_owned()),
        }

        if let Some(message) = &self.last_message {
            lines.push(format!("message: {message}"));
        }

        lines
    }

    fn analytics_summary_line(&self, analytics: &TuiActuatorAnalytics) -> String {
        let age = analytics
            .age_ticks(self.tick)
            .map(|ticks| format!("{ticks}t"))
            .unwrap_or_else(|| "n/a".to_owned());
        format!(
            "samples={} age={} faults={}",
            analytics.samples, age, analytics.fault_samples
        )
    }

    fn analytics_range_line(
        &self,
        snapshot: &ActuatorSnapshot,
        analytics: &TuiActuatorAnalytics,
    ) -> Option<String> {
        let mut parts = Vec::new();
        if let Some(span) = analytics.position_span() {
            parts.push(format!(
                "span={span:.3} {}",
                position_unit(snapshot.config.protocol)
            ));
        }
        if let Some(peak) = analytics.peak_abs_effort {
            parts.push(format!(
                "peak={peak:.2} {}",
                effort_unit(snapshot.config.protocol)
            ));
        }

        (!parts.is_empty()).then(|| parts.join(" "))
    }

    fn health_color(&self, health: ActuatorHealth) -> Color {
        match health {
            ActuatorHealth::Nominal => self.palette.high,
            ActuatorHealth::Warning => self.palette.pulse,
            ActuatorHealth::Fault => self.palette.pulse,
            ActuatorHealth::Stale => self.palette.mid,
        }
    }
}

fn snapshot_position(snapshot: &ActuatorSnapshot) -> Option<f64> {
    match snapshot.feedback.as_ref()? {
        ActuatorFeedback::CubeMars(feedback) => Some(feedback.position_degrees),
        ActuatorFeedback::RobStride(feedback) => Some(feedback.position_rad),
    }
}

fn snapshot_effort(snapshot: &ActuatorSnapshot) -> Option<f64> {
    match snapshot.feedback.as_ref()? {
        ActuatorFeedback::CubeMars(feedback) => Some(feedback.current_amps),
        ActuatorFeedback::RobStride(feedback) => Some(feedback.torque_nm),
    }
}

fn position_unit(protocol: ActuatorProtocol) -> &'static str {
    match protocol {
        ActuatorProtocol::CubeMars(_) => "deg",
        ActuatorProtocol::RobStride(_) => "rad",
    }
}

fn effort_unit(protocol: ActuatorProtocol) -> &'static str {
    match protocol {
        ActuatorProtocol::CubeMars(_) => "A",
        ActuatorProtocol::RobStride(_) => "Nm",
    }
}

fn draw_line(buffer: &mut Buffer, area: Rect, row: u16, text: &str, color: Color) {
    if row >= area.height || area.width == 0 {
        return;
    }

    let clipped = clip_text(text, usize::from(area.width));
    buffer.set_str(
        usize::from(area.x),
        usize::from(area.y.saturating_add(row)),
        &clipped,
        color,
        Some(Color::BLACK),
    );
}

fn clip_text(text: &str, width: usize) -> String {
    text.chars().take(width).collect()
}
