use crate::app::AppState;
use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph, Wrap};
use serde_json::Value;

const BRAND_PURPLE: Color = Color::Rgb(138, 43, 226);
const CYBER_CYAN: Color = Color::Rgb(0, 255, 255);
const NEON_GREEN: Color = Color::Rgb(57, 255, 20);
const AMBER_WARN: Color = Color::Rgb(255, 191, 0);
const TEXT_PRIMARY: Color = Color::Rgb(230, 230, 240);
const TEXT_DIM: Color = Color::Rgb(100, 100, 120);
const TEXT_MUTED: Color = Color::Rgb(60, 60, 80);
const BG_PANEL: Color = Color::Rgb(18, 18, 25);

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SentinelViewMode {
    Custom,
}

const ABOUT_SENTINEL_LINES: &[&str] = &[
    "Sentinel is 4Runr's runtime safety layer for agent executions on this Gateway.",
    "",
    "What it does:",
    "  • Watches each run while it is running (queue processor path)",
    "  • Enforces limits: idle time, max duration, token cap, cost, loop detection",
    "  • On violation: kills the run (status killed, policy_violation:* in logs)",
    "",
    "Scope:",
    "  • One policy for ALL runs on this Gateway (not per-agent yet)",
    "  • A run = one agent execution (POST /api/runs → start → agent runs)",
    "",
    "Persistence:",
    "  • Enter: saves to disk (%APPDATA%\\4runr\\config\\sentinel-limits.json)",
    "  • Survives Gateway restart",
    "  • Env vars in docker-compose override individual fields when set",
];

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CustomField {
    Enabled,
    IdleMs,
    MaxDurationMs,
    MaxTokens,
    MaxCost,
    LoopWindow,
    LoopMax,
}

impl CustomField {
    pub const ALL: [CustomField; 7] = [
        CustomField::Enabled,
        CustomField::IdleMs,
        CustomField::MaxDurationMs,
        CustomField::MaxTokens,
        CustomField::MaxCost,
        CustomField::LoopWindow,
        CustomField::LoopMax,
    ];

    pub fn label(self) -> &'static str {
        match self {
            CustomField::Enabled => "Enforcement",
            CustomField::IdleMs => "Idle timeout",
            CustomField::MaxDurationMs => "Max duration",
            CustomField::MaxTokens => "Max tokens",
            CustomField::MaxCost => "Max cost (USD)",
            CustomField::LoopWindow => "Loop window (sec)",
            CustomField::LoopMax => "Loop max repeats",
        }
    }

    pub fn env_var(self) -> &'static str {
        match self {
            CustomField::Enabled => "SENTINEL_ENABLED=true",
            CustomField::IdleMs => "SENTINEL_IDLE_MS or RUN_IDLE_MS",
            CustomField::MaxDurationMs => "RUN_MAX_DURATION_MS",
            CustomField::MaxTokens => "RUN_MAX_TOKENS",
            CustomField::MaxCost => "RUN_MAX_COST or SENTINEL_MAX_COST",
            CustomField::LoopWindow => "SENTINEL_LOOP_WINDOW",
            CustomField::LoopMax => "SENTINEL_LOOP_MAX",
        }
    }

    pub fn help(self) -> &'static str {
        match self {
            CustomField::Enabled => {
                "Master switch. When ON, Sentinel watches runs and kills them on policy violations. When OFF, no kills (monitoring may still log)."
            }
            CustomField::IdleMs => {
                "How long a run can sit with no token/activity events before Sentinel kills it. Your FRESH live test was killed by this (policy_violation:idle)."
            }
            CustomField::MaxDurationMs => {
                "Hard wall-clock limit from run start. Exceeding it triggers policy_violation:timeout regardless of activity."
            }
            CustomField::MaxTokens => {
                "Maximum cumulative LLM tokens for the run. Useful for cost and runaway generation control."
            }
            CustomField::MaxCost => {
                "Maximum estimated spend in USD for the run (from token events). Triggers policy_violation:cost."
            }
            CustomField::LoopWindow => {
                "Time window (seconds) Sentinel uses to detect repetitive/stuck output patterns."
            }
            CustomField::LoopMax => {
                "How many similar repetitions are allowed inside the loop window. Must be ≤ loop window."
            }
        }
    }

    pub fn kill_reason(self) -> Option<&'static str> {
        match self {
            CustomField::IdleMs => Some("policy_violation:idle"),
            CustomField::MaxDurationMs => Some("policy_violation:timeout"),
            CustomField::MaxTokens => Some("policy_violation:token_cap"),
            CustomField::MaxCost => Some("policy_violation:cost"),
            CustomField::LoopWindow | CustomField::LoopMax => Some("policy_violation:loop_detection"),
            CustomField::Enabled => None,
        }
    }
}

#[derive(Debug, Clone)]
pub struct SentinelTemplateRow {
    pub key: String,
    pub name: String,
    pub description: String,
    pub idle_ms: u64,
    pub duration_ms: u64,
    pub max_tokens: u64,
    pub max_cost: f64,
}

#[derive(Debug, Clone)]
pub struct SentinelConfigState {
    pub view_mode: SentinelViewMode,
    pub templates: Vec<SentinelTemplateRow>,
    pub selected_index: usize,
    pub custom_draft: SentinelCurrentConfig,
    pub custom_field: CustomField,
    pub current_enabled: bool,
    pub current_idle_ms: u64,
    pub current_duration_ms: u64,
    pub current_max_tokens: u64,
    pub current_max_cost: f64,
    pub current_loop_window: u64,
    pub current_loop_max: u64,
    pub watched_runs: u32,
    pub health_status: String,
    pub loading: bool,
    pub applying: bool,
    pub error: Option<String>,
    pub status_message: Option<String>,
}

impl Default for SentinelConfigState {
    fn default() -> Self {
        Self {
            view_mode: SentinelViewMode::Custom,
            templates: Vec::new(),
            selected_index: 0,
            custom_draft: SentinelCurrentConfig::default(),
            custom_field: CustomField::Enabled,
            current_enabled: true,
            current_idle_ms: 0,
            current_duration_ms: 0,
            current_max_tokens: 0,
            current_max_cost: 0.0,
            current_loop_window: 0,
            current_loop_max: 0,
            watched_runs: 0,
            health_status: String::new(),
            loading: false,
            applying: false,
            error: None,
            status_message: None,
        }
    }
}

impl SentinelConfigState {
    pub fn selected_template(&self) -> Option<&SentinelTemplateRow> {
        self.templates.get(self.selected_index)
    }

    pub fn cycle_view_mode(&mut self) {
        // Only one view now, so cycling does nothing
        self.view_mode = SentinelViewMode::Custom;
        self.sync_custom_draft_from_current();
        self.error = None;
        self.status_message = None;
    }

    pub fn sync_custom_draft_from_current(&mut self) {
        self.custom_draft = SentinelCurrentConfig {
            enabled: self.current_enabled,
            idle_ms: self.current_idle_ms,
            duration_ms: self.current_duration_ms,
            max_tokens: self.current_max_tokens,
            max_cost: self.current_max_cost,
            loop_window: self.current_loop_window,
            loop_max: self.current_loop_max,
        };
    }

    pub fn select_previous(&mut self) {
        let idx = CustomField::ALL
            .iter()
            .position(|f| *f == self.custom_field)
            .unwrap_or(0);
        if idx > 0 {
            self.custom_field = CustomField::ALL[idx - 1];
        }
    }

    pub fn select_next(&mut self) {
        let idx = CustomField::ALL
            .iter()
            .position(|f| *f == self.custom_field)
            .unwrap_or(0);
        if idx + 1 < CustomField::ALL.len() {
            self.custom_field = CustomField::ALL[idx + 1];
        }
    }

    pub fn adjust_custom_field(&mut self, increase: bool) {
        let d = &mut self.custom_draft;
        match self.custom_field {
            CustomField::Enabled => d.enabled = !d.enabled,
            CustomField::IdleMs => {
                let step = 5_000u64;
                if increase {
                    d.idle_ms = d.idle_ms.saturating_add(step).min(600_000);
                } else {
                    d.idle_ms = d.idle_ms.saturating_sub(step).max(step);
                }
            }
            CustomField::MaxDurationMs => {
                let step = 5_000u64;
                if increase {
                    d.duration_ms = d.duration_ms.saturating_add(step).min(3_600_000);
                } else {
                    d.duration_ms = d.duration_ms.saturating_sub(step).max(step);
                }
            }
            CustomField::MaxTokens => {
                let step = 1_000u64;
                if increase {
                    d.max_tokens = d.max_tokens.saturating_add(step).min(500_000);
                } else {
                    d.max_tokens = d.max_tokens.saturating_sub(step).max(1_000);
                }
            }
            CustomField::MaxCost => {
                let step = 0.05;
                if increase {
                    d.max_cost = round_cost((d.max_cost + step).min(100.0));
                } else {
                    d.max_cost = round_cost((d.max_cost - step).max(0.05));
                }
            }
            CustomField::LoopWindow => {
                if increase {
                    d.loop_window = d.loop_window.saturating_add(1).min(120);
                } else {
                    d.loop_window = d.loop_window.saturating_sub(1).max(1);
                }
                if d.loop_max > d.loop_window {
                    d.loop_max = d.loop_window;
                }
            }
            CustomField::LoopMax => {
                if increase {
                    d.loop_max = d.loop_max.saturating_add(1).min(d.loop_window.max(1));
                } else {
                    d.loop_max = d.loop_max.saturating_sub(1).max(1);
                }
            }
        }
    }

    pub fn apply_load(
        &mut self,
        templates: Vec<SentinelTemplateRow>,
        current: &SentinelCurrentConfig,
        health: &SentinelHealthSnapshot,
    ) {
        self.templates = templates;
        self.selected_index = 0;
        self.apply_current(current, None);
        self.watched_runs = health.watched_runs;
        self.health_status = health.status.clone();
        self.sync_custom_draft_from_current();
        self.loading = false;
        self.applying = false;
        self.error = None;
    }

    pub fn apply_current(&mut self, current: &SentinelCurrentConfig, message: Option<String>) {
        self.current_enabled = current.enabled;
        self.current_idle_ms = current.idle_ms;
        self.current_duration_ms = current.duration_ms;
        self.current_max_tokens = current.max_tokens;
        self.current_max_cost = current.max_cost;
        self.current_loop_window = current.loop_window;
        self.current_loop_max = current.loop_max;
        self.sync_custom_draft_from_current();
        self.applying = false;
        self.status_message = message;
        self.error = None;
    }
}

#[derive(Debug, Clone, Default)]
pub struct SentinelCurrentConfig {
    pub enabled: bool,
    pub idle_ms: u64,
    pub duration_ms: u64,
    pub max_tokens: u64,
    pub max_cost: f64,
    pub loop_window: u64,
    pub loop_max: u64,
}

#[derive(Debug, Clone, Default)]
pub struct SentinelHealthSnapshot {
    pub watched_runs: u32,
    pub status: String,
}

pub fn parse_sentinel_load(
    templates: &[Value],
    current: &Value,
    health: &Value,
) -> (
    Vec<SentinelTemplateRow>,
    SentinelCurrentConfig,
    SentinelHealthSnapshot,
) {
    let rows: Vec<SentinelTemplateRow> = templates
        .iter()
        .filter_map(|t| {
            let o = t.as_object()?;
            let key = o
                .get("key")
                .and_then(|v| v.as_str())
                .or_else(|| o.get("name").and_then(|v| v.as_str()))?
                .to_string();
            let name = o
                .get("name")
                .and_then(|v| v.as_str())
                .unwrap_or(&key)
                .to_string();
            let description = o
                .get("description")
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .to_string();
            let cfg = o.get("config").and_then(|v| v.as_object());
            let idle_ms = cfg
                .and_then(|c| c.get("runIdleMs"))
                .and_then(|v| v.as_u64())
                .unwrap_or(0);
            let duration_ms = cfg
                .and_then(|c| c.get("runMaxDurationMs"))
                .and_then(|v| v.as_u64())
                .unwrap_or(0);
            let max_tokens = cfg
                .and_then(|c| c.get("runMaxTokens"))
                .and_then(|v| v.as_u64())
                .unwrap_or(0);
            let max_cost = cfg
                .and_then(|c| c.get("runMaxCost"))
                .and_then(|v| v.as_f64())
                .unwrap_or(0.0);
            Some(SentinelTemplateRow {
                key,
                name,
                description,
                idle_ms,
                duration_ms,
                max_tokens,
                max_cost,
            })
        })
        .collect();

    (rows, parse_current_config(current), parse_health(health))
}

pub fn parse_current_config(v: &Value) -> SentinelCurrentConfig {
    let o = match v.as_object() {
        Some(o) => o,
        None => return SentinelCurrentConfig::default(),
    };
    SentinelCurrentConfig {
        enabled: o.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true),
        idle_ms: o.get("runIdleMs").and_then(|v| v.as_u64()).unwrap_or(30_000),
        duration_ms: o
            .get("runMaxDurationMs")
            .and_then(|v| v.as_u64())
            .unwrap_or(120_000),
        max_tokens: o
            .get("runMaxTokens")
            .and_then(|v| v.as_u64())
            .unwrap_or(16_000),
        max_cost: o.get("runMaxCost").and_then(|v| v.as_f64()).unwrap_or(1.0),
        loop_window: o.get("loopWindow").and_then(|v| v.as_u64()).unwrap_or(15),
        loop_max: o.get("loopMax").and_then(|v| v.as_u64()).unwrap_or(5),
    }
}

pub fn parse_health(v: &Value) -> SentinelHealthSnapshot {
    let o = match v.as_object() {
        Some(o) => o,
        None => return SentinelHealthSnapshot::default(),
    };
    SentinelHealthSnapshot {
        watched_runs: o.get("watchedRuns").and_then(|v| v.as_u64()).unwrap_or(0) as u32,
        status: o
            .get("status")
            .and_then(|v| v.as_str())
            .unwrap_or("unknown")
            .to_string(),
    }
}

fn fmt_ms(ms: u64) -> String {
    let total_secs = ms / 1000;
    let mins = total_secs / 60;
    let secs = total_secs % 60;
    if mins > 0 {
        format!("{}:{:02}", mins, secs)
    } else {
        format!("{}s", secs)
    }
}

fn round_cost(usd: f64) -> f64 {
    (usd * 100.0).round() / 100.0
}

fn fmt_cost(usd: f64) -> String {
    let cents = (round_cost(usd) * 100.0).round() as i64;
    let dollars = cents / 100;
    let rem = cents.abs() % 100;
    if rem == 0 {
        format!("${}", dollars)
    } else {
        format!("${}.{:02}", dollars, rem)
    }
}

fn custom_field_value(d: &SentinelCurrentConfig, field: CustomField) -> String {
    match field {
        CustomField::Enabled => {
            if d.enabled {
                "ON".to_string()
            } else {
                "OFF".to_string()
            }
        }
        CustomField::IdleMs => fmt_ms(d.idle_ms),
        CustomField::MaxDurationMs => fmt_ms(d.duration_ms),
        CustomField::MaxTokens => format!("{}", d.max_tokens),
        CustomField::MaxCost => fmt_cost(d.max_cost),
        CustomField::LoopWindow => format!("{}s", d.loop_window),
        CustomField::LoopMax => format!("{}", d.loop_max),
    }
}

pub fn render(f: &mut Frame, state: &AppState) {
    let area = f.size();
    let sc = &state.sentinel_config;

    use ratatui::layout::{Constraint, Direction, Layout};

    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Length(3),
            Constraint::Min(8),
            Constraint::Length(3),
        ])
        .split(area);

    let health_txt = if sc.health_status.is_empty() {
        "—".to_string()
    } else {
        format!("{} · {} watched", sc.health_status, sc.watched_runs)
    };

    let header = Block::default()
        .title(format!(" 🛡️  Sentinel — {} ", health_txt))
        .borders(Borders::ALL)
        .border_style(Style::default().fg(BRAND_PURPLE))
        .style(Style::default().bg(BG_PANEL));
    f.render_widget(header, chunks[0]);

    let body = Layout::default()
        .direction(Direction::Horizontal)
        .constraints([Constraint::Percentage(45), Constraint::Percentage(55)])
        .split(chunks[1]);

    // Unified view: Custom editor on left, About + Help on right
    render_custom_editor(f, body[0], sc);
    render_custom_help_with_about(f, body[1], sc);

    const DEFAULT_FOOTER: &str =
        "↑/↓ Field · ←/→ Value · Enter Apply (saves to disk) · R Refresh · ESC Close";
    const CUSTOM_SHORTCUTS: &str =
        "←/→ or +/- adjust value · wheel · Space ON/OFF · Enter apply · ESC close";

    let msg = if sc.loading {
        "Loading…".to_string()
    } else if sc.applying {
        "Applying…".to_string()
    } else if let Some(e) = &sc.error {
        e.clone()
    } else if let Some(m) = &sc.status_message {
        format!("{}\n{}", m, CUSTOM_SHORTCUTS)
    } else {
        DEFAULT_FOOTER.to_string()
    };

    let footer = Block::default()
        .title(" ⌨️  Actions ")
        .borders(Borders::ALL)
        .border_style(Style::default().fg(TEXT_MUTED))
        .style(Style::default().bg(BG_PANEL));
    let inner = footer.inner(chunks[2]);
    f.render_widget(footer, chunks[2]);
    let color = if sc.error.is_some() {
        Color::Rgb(255, 69, 69)
    } else if sc.status_message.is_some() {
        NEON_GREEN
    } else {
        TEXT_DIM
    };
    f.render_widget(
        Paragraph::new(msg)
            .style(Style::default().fg(color))
            .alignment(Alignment::Center),
        inner,
    );
}

fn render_about_panel(f: &mut Frame, area: Rect, sc: &SentinelConfigState) {
    let block = Block::default()
        .title(" 📖  What is Sentinel? ")
        .borders(Borders::ALL)
        .border_style(Style::default().fg(CYBER_CYAN))
        .style(Style::default().bg(BG_PANEL));
    let inner = block.inner(area);
    f.render_widget(block, area);

    let mut lines: Vec<Line> = Vec::new();
    for line in ABOUT_SENTINEL_LINES {
        if line.is_empty() {
            lines.push(Line::from(""));
            continue;
        }
        let style = if line.starts_with("  •") {
            Style::default().fg(TEXT_PRIMARY)
        } else if line.ends_with(':') && !line.starts_with(' ') {
            Style::default().fg(NEON_GREEN).bold()
        } else {
            Style::default().fg(TEXT_DIM)
        };
        lines.push(Line::from(Span::styled(*line, style)));
    }
    lines.push(Line::from(""));
    lines.push(Line::from(vec![
        Span::styled("Active limits now: ", Style::default().fg(TEXT_DIM)),
        Span::styled(
            if sc.current_enabled { "ON" } else { "OFF" },
            Style::default().fg(NEON_GREEN),
        ),
        Span::styled(" · idle ", Style::default().fg(TEXT_DIM)),
        Span::styled(fmt_ms(sc.current_idle_ms), Style::default().fg(TEXT_PRIMARY)),
        Span::styled(" · max ", Style::default().fg(TEXT_DIM)),
        Span::styled(fmt_ms(sc.current_duration_ms), Style::default().fg(TEXT_PRIMARY)),
        Span::styled(" · cost ", Style::default().fg(TEXT_DIM)),
        Span::styled(fmt_cost(sc.current_max_cost), Style::default().fg(AMBER_WARN)),
    ]));

    f.render_widget(Paragraph::new(lines).wrap(Wrap { trim: false }), inner);
}

fn render_active_panel(f: &mut Frame, area: Rect, sc: &SentinelConfigState) {
    let block = Block::default()
        .title(" ⚙️  Active limits (live) ")
        .borders(Borders::ALL)
        .border_style(Style::default().fg(CYBER_CYAN))
        .style(Style::default().bg(BG_PANEL));
    let inner = block.inner(area);
    f.render_widget(block, area);

    if sc.loading && sc.current_idle_ms == 0 {
        f.render_widget(
            Paragraph::new("Loading…").style(Style::default().fg(TEXT_DIM)),
            inner,
        );
        return;
    }

    let enabled = if sc.current_enabled { "ON" } else { "OFF" };
    let lines = vec![
        Line::from(""),
        Line::from(vec![
            Span::styled("Enforcement: ", Style::default().fg(TEXT_DIM)),
            Span::styled(enabled, Style::default().fg(NEON_GREEN).bold()),
        ]),
        Line::from(vec![
            Span::styled("Idle timeout: ", Style::default().fg(TEXT_DIM)),
            Span::styled(fmt_ms(sc.current_idle_ms), Style::default().fg(TEXT_PRIMARY)),
        ]),
        Line::from(vec![
            Span::styled("Max duration: ", Style::default().fg(TEXT_DIM)),
            Span::styled(
                fmt_ms(sc.current_duration_ms),
                Style::default().fg(TEXT_PRIMARY),
            ),
        ]),
        Line::from(vec![
            Span::styled("Max tokens: ", Style::default().fg(TEXT_DIM)),
            Span::styled(
                format!("{}", sc.current_max_tokens),
                Style::default().fg(TEXT_PRIMARY),
            ),
        ]),
        Line::from(vec![
            Span::styled("Max cost: ", Style::default().fg(TEXT_DIM)),
            Span::styled(fmt_cost(sc.current_max_cost), Style::default().fg(AMBER_WARN)),
        ]),
        Line::from(vec![
            Span::styled("Loop detect: ", Style::default().fg(TEXT_DIM)),
            Span::styled(
                format!("{} repeats in {}s", sc.current_loop_max, sc.current_loop_window),
                Style::default().fg(TEXT_PRIMARY),
            ),
        ]),
        Line::from(""),
        Line::from("Tab → About / Custom / Templates to switch views.").style(Style::default().fg(TEXT_MUTED)),
    ];
    f.render_widget(Paragraph::new(lines).wrap(Wrap { trim: false }), inner);
}

fn render_templates_panel(f: &mut Frame, area: Rect, sc: &SentinelConfigState) {
    let block = Block::default()
        .title(" 📋 Presets ")
        .borders(Borders::ALL)
        .border_style(Style::default().fg(NEON_GREEN))
        .style(Style::default().bg(BG_PANEL));
    let inner = block.inner(area);
    f.render_widget(block, area);

    if sc.templates.is_empty() {
        f.render_widget(
            Paragraph::new(if sc.loading {
                "Loading templates…"
            } else {
                "Connect to Gateway"
            })
            .style(Style::default().fg(TEXT_DIM)),
            inner,
        );
        return;
    }

    let items: Vec<ListItem> = sc
        .templates
        .iter()
        .enumerate()
        .map(|(i, t)| {
            let sel = i == sc.selected_index;
            let prefix = if sel { "▶ " } else { "  " };
            ListItem::new(vec![
                Line::from(format!(
                    "{}{}  idle {} · max {}",
                    prefix,
                    t.name,
                    fmt_ms(t.idle_ms),
                    fmt_ms(t.duration_ms)
                ))
                .style(if sel {
                    Style::default().fg(CYBER_CYAN).bold()
                } else {
                    Style::default().fg(TEXT_PRIMARY)
                }),
                Line::from(format!("    {}", t.description)).style(Style::default().fg(TEXT_DIM)),
            ])
        })
        .collect();

    f.render_widget(List::new(items), inner);
}

fn render_custom_editor(f: &mut Frame, area: Rect, sc: &SentinelConfigState) {
    let block = Block::default()
        .title(" ✏️  Custom limits (draft) ")
        .borders(Borders::ALL)
        .border_style(Style::default().fg(AMBER_WARN))
        .style(Style::default().bg(BG_PANEL));
    let inner = block.inner(area);
    f.render_widget(block, area);

    let d = &sc.custom_draft;
    let items: Vec<ListItem> = CustomField::ALL
        .iter()
        .map(|field| {
            let sel = *field == sc.custom_field;
            let prefix = if sel { "▶ " } else { "  " };
            let val = custom_field_value(d, *field);
            ListItem::new(Line::from(format!(
                "{}{}: {}",
                prefix,
                field.label(),
                val
            ))
            .style(if sel {
                Style::default().fg(CYBER_CYAN).bold()
            } else {
                Style::default().fg(TEXT_PRIMARY)
            }))
        })
        .collect();

    f.render_widget(List::new(items), inner);
}

fn render_custom_help_with_about(f: &mut Frame, area: Rect, sc: &SentinelConfigState) {
    use ratatui::layout::{Constraint, Direction, Layout};

    // Split into About section (top) and field help (bottom)
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([Constraint::Length(15), Constraint::Min(10)])
        .split(area);

    // Top: About Sentinel
    let about_block = Block::default()
        .title(" 📖  What is Sentinel? ")
        .borders(Borders::ALL)
        .border_style(Style::default().fg(CYBER_CYAN))
        .style(Style::default().bg(BG_PANEL));
    let about_inner = about_block.inner(chunks[0]);
    f.render_widget(about_block, chunks[0]);

    let mut about_lines: Vec<Line> = Vec::new();
    for line in ABOUT_SENTINEL_LINES {
        if line.is_empty() {
            about_lines.push(Line::from(""));
            continue;
        }
        let style = if line.starts_with("  •") {
            Style::default().fg(TEXT_PRIMARY)
        } else if line.ends_with(':') && !line.starts_with(' ') {
            Style::default().fg(NEON_GREEN).bold()
        } else {
            Style::default().fg(TEXT_DIM)
        };
        about_lines.push(Line::from(Span::styled(*line, style)));
    }
    f.render_widget(
        Paragraph::new(about_lines).wrap(Wrap { trim: false }),
        about_inner,
    );

    // Bottom: Field-specific help
    let field = sc.custom_field;
    let help_block = Block::default()
        .title(format!(" ℹ️  {} ", field.label()))
        .borders(Borders::ALL)
        .border_style(Style::default().fg(BRAND_PURPLE))
        .style(Style::default().bg(BG_PANEL));
    let help_inner = help_block.inner(chunks[1]);
    f.render_widget(help_block, chunks[1]);

    let mut help_lines = vec![
        Line::from(""),
        Line::from(field.help()).style(Style::default().fg(TEXT_PRIMARY)),
        Line::from(""),
    ];
    if let Some(reason) = field.kill_reason() {
        help_lines.push(Line::from(vec![
            Span::styled("Kill log reason: ", Style::default().fg(TEXT_DIM)),
            Span::styled(reason, Style::default().fg(AMBER_WARN)),
        ]));
        help_lines.push(Line::from(""));
    }
    help_lines.push(
        Line::from("Persistent via env (Gateway boot):")
            .style(Style::default().fg(NEON_GREEN).bold()),
    );
    help_lines.push(Line::from(field.env_var()).style(Style::default().fg(CYBER_CYAN)));
    help_lines.push(Line::from(""));
    help_lines.push(
        Line::from("Adjust: ←/→ or +/- · mouse wheel · Space toggles Enforcement.")
            .style(Style::default().fg(NEON_GREEN)),
    );

    f.render_widget(
        Paragraph::new(help_lines).wrap(Wrap { trim: false }),
        help_inner,
    );
}

#[cfg(test)]
mod format_tests {
    use super::{fmt_cost, fmt_ms};

    #[test]
    fn fmt_ms_shows_minutes_and_seconds() {
        assert_eq!(fmt_ms(30_000), "30s");
        assert_eq!(fmt_ms(60_000), "1:00");
        assert_eq!(fmt_ms(75_000), "1:15");
        assert_eq!(fmt_ms(240_000), "4:00");
        assert_eq!(fmt_ms(255_000), "4:15");
    }

    #[test]
    fn fmt_cost_shows_cents_when_present() {
        assert_eq!(fmt_cost(1.0), "$1");
        assert_eq!(fmt_cost(1.05), "$1.05");
        assert_eq!(fmt_cost(1.15), "$1.15");
        assert_eq!(fmt_cost(1.25), "$1.25");
    }
}
