proses 0.1.1

Proses – Professional Secure Execution System
use chrono::Utc;
use ratatui::{
    layout::{Alignment, Constraint, Layout, Rect},
    style::{Color, Modifier, Style, Stylize},
    text::{Line, Span},
    widgets::{Block, Borders, Cell, Paragraph, Row, Table, Wrap},
    Frame,
};

use crate::process::ProcessStatus;
use crate::tui::app::{App, LogSource};

// ── Entry point ───────────────────────────────────────────────────────────────

/// Render one full frame: title bar → process table + log pane → status bar.
pub fn render(frame: &mut Frame, app: &mut App) {
    let area = frame.area();

    let chunks = Layout::vertical([
        Constraint::Length(3), // title / header
        Constraint::Fill(1),   // process table + log pane
        Constraint::Length(1), // status / help bar
    ])
    .split(area);

    render_title(frame, chunks[0], app);
    render_body(frame, chunks[1], app);
    render_statusbar(frame, chunks[2], app);
}

// ── Title bar ─────────────────────────────────────────────────────────────────

fn render_title(frame: &mut Frame, area: Rect, app: &App) {
    let online = app
        .processes
        .iter()
        .filter(|p| p.status == ProcessStatus::Running)
        .count();
    let stopped = app
        .processes
        .iter()
        .filter(|p| p.status == ProcessStatus::Stopped)
        .count();
    let errored = app
        .processes
        .iter()
        .filter(|p| p.status == ProcessStatus::Errored)
        .count();

    let title_line = Line::from(vec![
        Span::styled("  PROSES  ", Style::default().bold().fg(Color::White)),
        Span::styled("  ", Style::default()),
        Span::styled(
            format!("{online} online"),
            Style::default().fg(Color::Green).bold(),
        ),
        Span::styled("   ·   ", Style::default().fg(Color::DarkGray)),
        Span::styled(
            format!("{stopped} stopped"),
            if stopped > 0 {
                Style::default().fg(Color::Gray)
            } else {
                Style::default().fg(Color::DarkGray)
            },
        ),
        Span::styled("   ·   ", Style::default().fg(Color::DarkGray)),
        Span::styled(
            format!("{errored} errored"),
            if errored > 0 {
                Style::default().fg(Color::Red).bold()
            } else {
                Style::default().fg(Color::DarkGray)
            },
        ),
    ]);

    let block = Block::default()
        .borders(Borders::ALL)
        .border_style(Style::default().fg(Color::DarkGray))
        .title(title_line)
        .title_alignment(Alignment::Left)
        .title_bottom(
            Line::from(Span::styled(
                "  proses v0.1.0  ",
                Style::default().fg(Color::DarkGray),
            ))
            .right_aligned(),
        );

    frame.render_widget(block, area);
}

// ── Body: process table + log pane ───────────────────────────────────────────

fn render_body(frame: &mut Frame, area: Rect, app: &mut App) {
    let chunks = Layout::vertical([Constraint::Percentage(50), Constraint::Fill(1)]).split(area);

    render_process_table(frame, chunks[0], app);
    render_log_pane(frame, chunks[1], app);
}

fn render_process_table(frame: &mut Frame, area: Rect, app: &mut App) {
    let header_style = Style::default().bold().fg(Color::Cyan);
    let selected_style = Style::default().add_modifier(Modifier::REVERSED);

    let header = Row::new(vec![
        Cell::from(" ID"),
        Cell::from("Name"),
        Cell::from("Status"),
        Cell::from("PID"),
        Cell::from(""),
        Cell::from("Uptime"),
        Cell::from("Command"),
    ])
    .style(header_style)
    .bottom_margin(0);

    let rows: Vec<Row> = app
        .processes
        .iter()
        .map(|p| {
            let (symbol, status_style) = match p.status {
                ProcessStatus::Running => ("● online ", Style::default().fg(Color::Green).bold()),
                ProcessStatus::Stopped => ("○ stopped", Style::default().fg(Color::DarkGray)),
                ProcessStatus::Errored => ("✕ errored", Style::default().fg(Color::Red).bold()),
            };

            let uptime = if p.status == ProcessStatus::Running && p.pid > 0 {
                format_uptime(p.started_at)
            } else {
                "".to_string()
            };

            let pid_str = if p.pid > 0 {
                p.pid.to_string()
            } else {
                "".to_string()
            };

            Row::new(vec![
                Cell::from(format!(" {}", p.id)),
                Cell::from(p.name.clone()),
                Cell::from(Span::styled(symbol, status_style)),
                Cell::from(pid_str),
                Cell::from(p.restarts.to_string()),
                Cell::from(uptime),
                Cell::from(truncate(&p.command, 48)),
            ])
        })
        .collect();

    let widths = [
        Constraint::Length(5),  //  ID
        Constraint::Length(18), // Name
        Constraint::Length(11), // Status
        Constraint::Length(8),  // PID
        Constraint::Length(4),  //        Constraint::Length(10), // Uptime
        Constraint::Fill(1),    // Command
    ];

    let table = Table::new(rows, widths)
        .header(header)
        .block(
            Block::default()
                .borders(Borders::ALL)
                .border_style(Style::default().fg(Color::DarkGray))
                .title(Span::styled(
                    " Processes ",
                    Style::default().fg(Color::White).bold(),
                )),
        )
        .row_highlight_style(selected_style)
        .highlight_symbol("");

    frame.render_stateful_widget(table, area, &mut app.table_state);
}

fn render_log_pane(frame: &mut Frame, area: Rect, app: &App) {
    let proc_name = app
        .selected_process()
        .map(|p| p.name.as_str())
        .unwrap_or("none");

    let src_label = match app.log_source {
        LogSource::Stdout => "stdout",
        LogSource::Stderr => "stderr",
    };

    let title = format!(" Logs: {proc_name} [{src_label}] ");

    let content: Vec<Line> = if app.log_lines.is_empty() {
        vec![Line::from(Span::styled(
            "  (no output yet)",
            Style::default().fg(Color::DarkGray),
        ))]
    } else {
        app.log_lines
            .iter()
            .map(|l| Line::from(Span::raw(format!("  {l}"))))
            .collect()
    };

    // Auto-scroll: always show the most recent lines at the bottom.
    let total_lines = content.len() as u16;
    let inner_height = area.height.saturating_sub(2); // subtract borders
    let scroll_offset = total_lines.saturating_sub(inner_height);

    let para = Paragraph::new(content)
        .block(
            Block::default()
                .borders(Borders::ALL)
                .border_style(Style::default().fg(Color::DarkGray))
                .title(Span::styled(
                    title,
                    Style::default().fg(Color::White).bold(),
                )),
        )
        .wrap(Wrap { trim: false })
        .scroll((scroll_offset, 0));

    frame.render_widget(para, area);
}

// ── Status / help bar ─────────────────────────────────────────────────────────

fn render_statusbar(frame: &mut Frame, area: Rect, app: &App) {
    let line = if app.confirm_delete {
        Line::from(vec![Span::styled(
            "  ⚠  Press d again to confirm deletion  ·  Esc to cancel",
            Style::default().fg(Color::Yellow).bold(),
        )])
    } else if let Some((msg, is_error)) = app.status_text() {
        let color = if is_error { Color::Red } else { Color::Green };
        Line::from(Span::styled(format!("  {msg}"), Style::default().fg(color)))
    } else {
        help_line()
    };

    frame.render_widget(Paragraph::new(line), area);
}

fn help_line<'a>() -> Line<'a> {
    let dim = Style::default().fg(Color::DarkGray);
    let key = Style::default().fg(Color::White);

    Line::from(vec![
        Span::styled("  ", dim),
        Span::styled("↑↓", key),
        Span::styled(" navigate", dim),
        Span::styled("   r", key),
        Span::styled(" restart", dim),
        Span::styled("   s", key),
        Span::styled(" stop/start", dim),
        Span::styled("   d", key),
        Span::styled(" delete", dim),
        Span::styled("   e", key),
        Span::styled(" stderr/stdout", dim),
        Span::styled("   q", key),
        Span::styled(" quit", dim),
    ])
}

// ── Utilities ─────────────────────────────────────────────────────────────────

fn truncate(s: &str, max: usize) -> String {
    let chars: Vec<char> = s.chars().collect();
    if chars.len() <= max {
        s.to_string()
    } else {
        let truncated: String = chars[..max - 1].iter().collect();
        format!("{truncated}")
    }
}

fn format_uptime(started_at: chrono::DateTime<Utc>) -> String {
    let secs = (Utc::now() - started_at).num_seconds().max(0) as u64;

    if secs < 60 {
        format!("{secs}s")
    } else if secs < 3600 {
        let m = secs / 60;
        let s = secs % 60;
        format!("{m}m {s}s")
    } else if secs < 86_400 {
        let h = secs / 3600;
        let m = (secs % 3600) / 60;
        format!("{h}h {m}m")
    } else {
        let d = secs / 86_400;
        let h = (secs % 86_400) / 3600;
        format!("{d}d {h}h")
    }
}