proses 0.1.1

Proses – Professional Secure Execution System
use std::time::{Duration, Instant};

use ratatui::widgets::TableState;

use crate::{
    daemon::ipc::{self, Request},
    process::{Process, ProcessStatus},
};

/// Which log stream is currently visible in the log pane.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogSource {
    Stdout,
    Stderr,
}

/// A transient notification shown in the help / status bar.
struct StatusMsg {
    text: String,
    is_error: bool,
    at: Instant,
}

/// Top-level application state passed into every render frame.
pub struct App {
    /// Current snapshot of all managed processes (refreshed every 2 s).
    pub processes: Vec<Process>,

    /// Ratatui stateful-widget state – tracks the highlighted row index.
    pub table_state: TableState,

    /// Lines loaded from the selected process's log file (~last 200 lines).
    pub log_lines: Vec<String>,

    /// Whether the log pane shows stdout or stderr.
    pub log_source: LogSource,

    /// Set to `true` when the user presses `q` / `Esc` to exit.
    pub should_quit: bool,

    /// Waiting for the second `d` keypress to confirm deletion.
    pub confirm_delete: bool,

    /// When the last full refresh from the daemon happened.
    pub last_refresh: Instant,

    /// Optional transient message displayed in the status bar.
    status: Option<StatusMsg>,
}

impl App {
    /// Create a fresh, empty `App`.
    /// Call [`App::refresh`] right away to populate it with live data.
    pub fn new() -> Self {
        Self {
            processes: Vec::new(),
            table_state: TableState::default(),
            log_lines: Vec::new(),
            log_source: LogSource::Stdout,
            should_quit: false,
            confirm_delete: false,
            last_refresh: Instant::now(),
            status: None,
        }
    }

    // ── Daemon communication ─────────────────────────────────────────────────

    /// Pull a fresh process list from the daemon and reload the log pane.
    pub fn refresh(&mut self) {
        match ipc::send(&Request::List) {
            Ok(resp) if resp.success => {
                if let Some(procs) = resp.processes {
                    self.processes = procs;
                    self.clamp_selection();
                }
            }
            Ok(resp) => self.set_error(resp.message),
            Err(e) => self.set_error(format!("Daemon unreachable: {e}")),
        }
        self.reload_logs();
        self.last_refresh = Instant::now();
    }

    /// Restart the currently selected process.
    pub fn restart_selected(&mut self) {
        let Some(id) = self.selected_id_str() else {
            return;
        };
        match ipc::send(&Request::Restart {
            name_or_id: id.clone(),
        }) {
            Ok(resp) if resp.success => self.set_ok(format!("↺  Restarted '{id}'")),
            Ok(resp) => self.set_error(resp.message),
            Err(e) => self.set_error(format!("IPC error: {e}")),
        }
        self.refresh();
    }

    /// Toggle the selected process:
    /// - Running        → Stop (will not auto-restart)
    /// - Stopped/Errored → Restart (re-launch)
    pub fn toggle_stop(&mut self) {
        let Some(id) = self.selected_id_str() else {
            return;
        };
        let is_running = self
            .selected_process()
            .map(|p| p.status == ProcessStatus::Running)
            .unwrap_or(false);

        let req = if is_running {
            Request::Stop {
                name_or_id: id.clone(),
            }
        } else {
            Request::Restart {
                name_or_id: id.clone(),
            }
        };

        match ipc::send(&req) {
            Ok(resp) if resp.success => self.set_ok(resp.message),
            Ok(resp) => self.set_error(resp.message),
            Err(e) => self.set_error(format!("IPC error: {e}")),
        }
        self.refresh();
    }

    /// Arm or execute a deletion.
    /// First `d`  → sets `confirm_delete = true` (UI shows a warning).
    /// Second `d` → sends the Delete request to the daemon.
    pub fn delete_selected(&mut self) {
        if !self.confirm_delete {
            self.confirm_delete = true;
            return;
        }
        self.confirm_delete = false;
        let Some(id) = self.selected_id_str() else {
            return;
        };
        match ipc::send(&Request::Delete {
            name_or_id: id.clone(),
        }) {
            Ok(resp) if resp.success => self.set_ok(format!("✕  Deleted '{id}'")),
            Ok(resp) => self.set_error(resp.message),
            Err(e) => self.set_error(format!("IPC error: {e}")),
        }
        self.refresh();
    }

    /// Flip between stdout and stderr in the log pane.
    pub fn toggle_log_source(&mut self) {
        self.log_source = match self.log_source {
            LogSource::Stdout => LogSource::Stderr,
            LogSource::Stderr => LogSource::Stdout,
        };
        self.reload_logs();
    }

    // ── Navigation ───────────────────────────────────────────────────────────

    pub fn select_next(&mut self) {
        if self.processes.is_empty() {
            return;
        }
        let next = match self.table_state.selected() {
            Some(i) if i + 1 < self.processes.len() => i + 1,
            _ => 0,
        };
        self.table_state.select(Some(next));
        self.reload_logs();
    }

    pub fn select_prev(&mut self) {
        if self.processes.is_empty() {
            return;
        }
        let prev = match self.table_state.selected() {
            Some(0) | None => self.processes.len() - 1,
            Some(i) => i - 1,
        };
        self.table_state.select(Some(prev));
        self.reload_logs();
    }

    // ── Accessors ────────────────────────────────────────────────────────────

    /// Return an immutable reference to the currently selected process, if any.
    pub fn selected_process(&self) -> Option<&Process> {
        self.table_state
            .selected()
            .and_then(|i| self.processes.get(i))
    }

    /// Return the numeric ID of the selected process as a string (used for IPC).
    fn selected_id_str(&self) -> Option<String> {
        self.selected_process().map(|p| p.id.to_string())
    }

    // ── Log loading ──────────────────────────────────────────────────────────

    /// Read the appropriate log file for the selected process and populate
    /// `self.log_lines` with its last ~200 lines.
    pub fn reload_logs(&mut self) {
        self.log_lines.clear();

        let Some(proc) = self.selected_process() else {
            return;
        };

        let path = match self.log_source {
            LogSource::Stdout => proc.log_out.clone(),
            LogSource::Stderr => proc.log_err.clone(),
        };

        if let Ok(content) = std::fs::read_to_string(&path) {
            // Take the last 200 lines so the buffer stays small.
            let lines: Vec<String> = content
                .lines()
                .rev()
                .take(200)
                .map(String::from)
                .collect::<Vec<_>>()
                .into_iter()
                .rev()
                .collect();
            self.log_lines = lines;
        }
    }

    // ── Selection clamping ───────────────────────────────────────────────────

    /// Ensure the selection index is valid after a process list update.
    /// Selects index 0 if nothing was selected before.
    fn clamp_selection(&mut self) {
        if self.processes.is_empty() {
            self.table_state.select(None);
            return;
        }
        match self.table_state.selected() {
            None => self.table_state.select(Some(0)),
            Some(i) if i >= self.processes.len() => {
                self.table_state.select(Some(self.processes.len() - 1));
            }
            _ => {}
        }
    }

    // ── Status messages ──────────────────────────────────────────────────────

    fn set_ok(&mut self, msg: String) {
        self.status = Some(StatusMsg {
            text: msg,
            is_error: false,
            at: Instant::now(),
        });
    }

    fn set_error(&mut self, msg: String) {
        self.status = Some(StatusMsg {
            text: msg,
            is_error: true,
            at: Instant::now(),
        });
    }

    /// Return the current status message if it is still within the 4-second
    /// display window.  Returns `(text, is_error)`.
    pub fn status_text(&self) -> Option<(&str, bool)> {
        self.status.as_ref().and_then(|s| {
            if s.at.elapsed() < Duration::from_secs(4) {
                Some((s.text.as_str(), s.is_error))
            } else {
                None
            }
        })
    }
}