// SPDX-License-Identifier: GPL-2.0

//! Rendering. Almost a pure function of the [`App`] state — the one
//! exception is the log view writing its viewport height and clamped scroll
//! offset back into the app, so paging and bounds always match the actual
//! terminal size.

use ratatui::Frame;
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Wrap};

use crate::app::{App, ArgsInput, View};
use crate::logs;

const SCHED_PREFIX: &str = "scx_";

pub fn draw(frame: &mut Frame, app: &mut App) {
    let [header, body, footer] = Layout::new(
        Direction::Vertical,
        [
            Constraint::Length(1),
            Constraint::Min(0),
            Constraint::Length(3),
        ],
    )
    .areas(frame.area());

    draw_header(frame, app, header);

    match app.view {
        View::Schedulers => {
            let [left, right] = Layout::new(
                Direction::Horizontal,
                [Constraint::Percentage(40), Constraint::Percentage(60)],
            )
            .areas(body);
            draw_scheduler_list(frame, app, left);
            draw_status_panel(frame, app, right);
        }
        View::Logs => draw_logs(frame, app, body),
    }

    draw_footer(frame, app, footer);
}

fn draw_header(frame: &mut Frame, app: &App, area: Rect) {
    let line = Line::from(vec![
        Span::styled(
            " scxtui ",
            Style::default()
                .fg(Color::Black)
                .bg(Color::Cyan)
                .add_modifier(Modifier::BOLD),
        ),
        Span::raw(" backend: "),
        Span::styled(app.backend_label(), Style::default().fg(Color::Cyan)),
    ]);
    frame.render_widget(Paragraph::new(line), area);
}

fn draw_scheduler_list(frame: &mut Frame, app: &App, area: Rect) {
    let current = app
        .status
        .as_ref()
        .and_then(|status| status.current.as_deref());

    let items: Vec<ListItem> = app
        .schedulers
        .iter()
        .map(|sched| {
            let is_running = Some(sched.as_str()) == current;
            let marker = if is_running { "● " } else { "  " };
            let style = if is_running {
                Style::default()
                    .fg(Color::Green)
                    .add_modifier(Modifier::BOLD)
            } else {
                Style::default()
            };
            ListItem::new(Line::from(vec![
                Span::styled(marker, Style::default().fg(Color::Green)),
                Span::styled(strip_prefix(sched), style),
            ]))
        })
        .collect();

    let list = List::new(items)
        .block(Block::default().borders(Borders::ALL).title(" Schedulers "))
        .highlight_style(
            Style::default()
                .bg(Color::DarkGray)
                .add_modifier(Modifier::BOLD),
        )
        .highlight_symbol("▶ ");

    let mut state = ListState::default().with_selected(Some(app.selected));
    frame.render_stateful_widget(list, area, &mut state);
}

fn draw_status_panel(frame: &mut Frame, app: &App, area: Rect) {
    let mut lines: Vec<Line> = Vec::new();

    match &app.status {
        Some(status) => match &status.current {
            Some(sched) if !status.args.is_empty() => {
                lines.push(kv("State", "running", Color::Green));
                lines.push(kv("Scheduler", strip_prefix(sched), Color::White));
                lines.push(kv("Arguments", &format_args(&status.args), Color::White));
            }
            Some(sched) => {
                lines.push(kv("State", "running", Color::Green));
                lines.push(kv("Scheduler", strip_prefix(sched), Color::White));
                lines.push(kv("Mode", mode_name(status.mode), Color::White));
            }
            None => lines.push(kv("State", "no scheduler running", Color::Yellow)),
        },
        None => lines.push(kv("State", "unknown", Color::Red)),
    }

    if let Some(status) = &app.status {
        let default = match &status.default_sched {
            Some(sched) => format!(
                "{} ({})",
                strip_prefix(sched),
                mode_name(status.default_mode)
            ),
            None => "not configured".to_owned(),
        };
        lines.push(kv("Default", &default, Color::White));
    }

    lines.push(Line::default());
    draw_kernel_section(app, &mut lines);

    lines.push(Line::default());
    if app.capabilities().modes {
        lines.push(Line::from(vec![
            Span::styled("Mode: ", Style::default().add_modifier(Modifier::BOLD)),
            Span::styled(
                format!("◀ {} ▶", mode_name(app.selected_mode())),
                Style::default().fg(Color::Cyan),
            ),
            Span::raw("  (Tab to cycle)"),
        ]));
        // Preview of what starting/switching in this mode would actually
        // pass to the scheduler. Rendered only when the answer is known
        // and non-empty: an unknown answer must not pretend to be "no
        // arguments", and an empty one is covered by the note below (or,
        // for Auto, by the scheduler's own defaults being the point).
        // The surrounding paragraph wraps, so long flag lists fold instead
        // of clipping.
        if let Some(args) = app.selected_mode_args().filter(|args| !args.is_empty()) {
            lines.push(Line::from(vec![
                Span::raw("  "),
                Span::styled(format_args(args), Style::default().fg(Color::Gray)),
            ]));
        }
        if !app.selected_mode_configured() {
            lines.push(Line::from(Span::styled(
                "  no configured arguments for this mode — scheduler defaults will be used",
                Style::default().fg(Color::Yellow),
            )));
        }
    } else {
        lines.push(Line::from(Span::styled(
            "Mode: not supported by this backend",
            Style::default().fg(Color::DarkGray),
        )));
    }

    if let Some(input) = &app.args_input {
        lines.push(Line::default());
        let mut spans = vec![Span::styled(
            "Args: ",
            Style::default().add_modifier(Modifier::BOLD),
        )];
        spans.extend(input_spans(input));
        lines.push(Line::from(spans));
        lines.push(Line::from(Span::styled(
            "  session-only — not written to the loader config; \
same syntax as scxctl --args",
            Style::default().fg(Color::DarkGray),
        )));
    }

    let panel = Paragraph::new(lines)
        .wrap(Wrap { trim: false })
        .block(Block::default().borders(Borders::ALL).title(" Status "));
    frame.render_widget(panel, area);
}

fn draw_logs(frame: &mut Frame, app: &mut App, area: Rect) {
    let height = area.height.saturating_sub(2) as usize;
    app.log_page = height.max(1);

    let total = app.log_lines.len();
    let max_scroll = total.saturating_sub(height);
    // Clamp and write back, so `g` (jump to top, scroll = MAX) and paging
    // past the ends settle on real bounds.
    if app.log_scroll > max_scroll {
        app.log_scroll = max_scroll;
    }
    let end = total - app.log_scroll;
    let start = end.saturating_sub(height);

    let lines: Vec<Line> = app.log_lines[start..end]
        .iter()
        .map(|line| {
            if line.continuation {
                Line::from(Span::styled(
                    format!("           ↳ {}", line.text),
                    Style::default().fg(Color::DarkGray),
                ))
            } else {
                Line::from(vec![
                    Span::styled(
                        format!("{:8} ", line.time),
                        Style::default().fg(Color::DarkGray),
                    ),
                    Span::styled(line.text.clone(), priority_style(line.priority)),
                ])
            }
        })
        .collect();

    let boot = if app.log_previous_boot {
        "-1 (previous)"
    } else {
        "0 (current)"
    };
    let title = format!(
        " Logs — {} · boot {} · {} lines ",
        logs::UNITS[app.log_unit],
        boot,
        total
    );

    let panel = Paragraph::new(lines).block(
        Block::default()
            .borders(Borders::ALL)
            .title(title)
            .title_style(Style::default().add_modifier(Modifier::BOLD)),
    );
    frame.render_widget(panel, area);
}

/// The kernel-truth section: what `sched_ext` itself reports, plus soft
/// warnings whenever it disagrees with what the loader claims. The loader
/// only knows about schedulers it started; the kernel knows what is
/// actually attached.
fn draw_kernel_section(app: &App, lines: &mut Vec<Line>) {
    let Some(kernel) = &app.kernel else {
        lines.push(kv(
            "Kernel",
            "sched_ext not available in this kernel",
            Color::DarkGray,
        ));
        return;
    };

    let ops_suffix = kernel
        .ops
        .as_deref()
        .map(|ops| format!(" · ops: {ops}"))
        .unwrap_or_default();
    let color = if kernel.enabled() {
        Color::Green
    } else {
        Color::White
    };
    lines.push(kv(
        "Kernel",
        &format!("sched_ext {}{}", kernel.state, ops_suffix),
        color,
    ));

    let loader_current = app
        .status
        .as_ref()
        .and_then(|status| status.current.as_deref());

    let warning = match (loader_current, kernel.enabled()) {
        // Loader thinks nothing runs, kernel disagrees: something was
        // started behind the loader's back (scx.service, a manual run).
        (None, true) => Some("a scheduler is attached outside of scx_loader's control".to_owned()),
        // Loader thinks a scheduler runs, kernel disagrees: it crashed or
        // was ejected (watchdog) without the loader noticing yet.
        (Some(sched), false) => Some(format!(
            "loader reports {sched}, but the kernel shows no attached scheduler"
        )),
        // Both agree something runs — but is it the same something?
        (Some(sched), true) if !kernel.matches(sched) => {
            let ops = kernel.ops.as_deref().unwrap_or("?");
            Some(format!(
                "loader reports {sched}, but the kernel ops name is \"{ops}\""
            ))
        }
        _ => None,
    };

    if let Some(text) = warning {
        lines.push(Line::from(Span::styled(
            format!("  ⚠ {text}"),
            Style::default().fg(Color::Yellow),
        )));
    }
}

fn priority_style(priority: u8) -> Style {
    match priority {
        0..=3 => Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
        4 => Style::default().fg(Color::Yellow),
        7 => Style::default().fg(Color::DarkGray),
        _ => Style::default(),
    }
}

fn draw_footer(frame: &mut Frame, app: &App, area: Rect) {
    let [keys_area, message_area] = Layout::new(
        Direction::Vertical,
        [Constraint::Length(1), Constraint::Length(2)],
    )
    .areas(area);

    let key_help = match app.view {
        View::Schedulers if app.args_input.is_some() => {
            String::from(" type arguments · Enter start/switch · Esc cancel")
        }
        View::Schedulers => {
            let caps = app.capabilities();
            let mut help = String::from(" ↑/↓ select · Tab mode · Enter start");
            if caps.live_switch {
                help.push_str("/switch");
            }
            if caps.custom_args {
                help.push_str(" · a args");
            }
            help.push_str(" · s stop · r restart");
            if caps.restore_default {
                help.push_str(" · d restore");
            }
            help.push_str(" · l logs · t monitor · B backend · R refresh · q quit");
            help
        }
        View::Logs => String::from(
            " ↑/↓ scroll · PgUp/PgDn page · g/G top/bottom · b boot · u unit · R reload · q/Esc back",
        ),
    };

    let keys = Line::from(Span::styled(key_help, Style::default().fg(Color::DarkGray)));
    frame.render_widget(Paragraph::new(keys), keys_area);

    if let Some(message) = &app.message {
        let color = if message.is_error {
            Color::Red
        } else {
            Color::Green
        };
        let line = Line::from(Span::styled(
            format!(" {}", message.text),
            Style::default().fg(color),
        ));
        frame.render_widget(
            Paragraph::new(line).wrap(Wrap { trim: false }),
            message_area,
        );
    }
}

fn kv<'a>(key: &'a str, value: &str, value_color: Color) -> Line<'a> {
    Line::from(vec![
        Span::styled(
            format!("{key}: "),
            Style::default().add_modifier(Modifier::BOLD),
        ),
        Span::styled(value.to_owned(), Style::default().fg(value_color)),
    ])
}

fn strip_prefix(sched: &str) -> &str {
    sched.strip_prefix(SCHED_PREFIX).unwrap_or(sched)
}

fn mode_name(mode: scx_loader::SchedMode) -> &'static str {
    <&str>::from(mode)
}

/// Renders the field content with a reversed-video block as the cursor.
/// A styled span instead of the real terminal cursor keeps the position
/// correct under the paragraph's wrapping without any coordinate math;
/// at the end of the buffer the block sits on a synthetic space.
fn input_spans(input: &ArgsInput) -> Vec<Span<'static>> {
    let mut before = String::new();
    let mut at = None;
    let mut after = String::new();
    for (i, c) in input.buffer.chars().enumerate() {
        match i.cmp(&input.cursor) {
            std::cmp::Ordering::Less => before.push(c),
            std::cmp::Ordering::Equal => at = Some(c),
            std::cmp::Ordering::Greater => after.push(c),
        }
    }
    let cursor = at.map_or_else(|| " ".to_owned(), |c| c.to_string());
    vec![
        Span::raw(before),
        Span::styled(cursor, Style::default().add_modifier(Modifier::REVERSED)),
        Span::raw(after),
    ]
}

/// Formats an argument vector exactly like scxctl renders scheduler
/// arguments: `shell_words::join`, so token boundaries survive spaces,
/// quotes and commas, and the rendered line parses back into the same
/// vector via `shell_words::split`.
fn format_args(args: &[String]) -> String {
    shell_words::join(args)
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Same guarantee scxctl pins for `format_scheduler_args`, repeated
    /// here for the panel renderer: what the preview shows must parse
    /// back into the identical argument vector.
    #[test]
    fn preview_format_round_trips() {
        let args = ["--name", "foo bar", "", "a'b", "foo,bar"]
            .map(str::to_string)
            .to_vec();

        assert_eq!(shell_words::split(&format_args(&args)), Ok(args));
    }
}
