//! Cross-platform, transport-neutral terminal operator interface.
//!
//! The reducer and renderer are terminal-free. [`run_terminal`] is the only
//! code which touches a terminal and uses crossterm's Windows/Unix backends.

#![allow(clippy::missing_errors_doc)]

use crate::control_plane::{
    AuditEventView, CONTROL_PROTOCOL_VERSION, ControlEvent, ControlEventKind, ControlPayload,
    ControlRequest, ControlRequestEnvelope, ControlResponseEnvelope, HealthView,
    InProcessControlClient, Page, PageRequest, PendingApprovalView, RuntimeView, ScheduledJobView,
    SessionMetadataView, TcpControlClient,
};
use crate::observability::{EventSeverity, MetricsSnapshot, StructuredEvent};
use crate::{AgentPreferences, ControlLimits, OperatingMode, PreferencesSummary, SecretString};
use crossterm::{cursor, event, execute, queue, style, terminal};
use std::collections::VecDeque;
use std::fmt;
use std::future::Future;
use std::io::{self, Write};
use std::net::SocketAddr;
use std::path::PathBuf;
use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};

const MAX_TIMELINE: usize = 2_000;
const MAX_ERRORS: usize = 100;
const PAGE_LIMIT: u16 = 100;

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum OperatorScreen {
    Overview,
    Sessions,
    QueuesAndBudgets,
    Roaming,
    Approvals,
    Timeline,
    Health,
    Errors,
    Diagnostics,
    Preferences,
}

impl OperatorScreen {
    const ALL: [Self; 10] = [
        Self::Overview,
        Self::Sessions,
        Self::QueuesAndBudgets,
        Self::Roaming,
        Self::Approvals,
        Self::Timeline,
        Self::Health,
        Self::Errors,
        Self::Diagnostics,
        Self::Preferences,
    ];

    const fn title(self) -> &'static str {
        match self {
            Self::Overview => "Overview",
            Self::Sessions => "Sessions",
            Self::QueuesAndBudgets => "Queues & budgets",
            Self::Roaming => "Roaming",
            Self::Approvals => "Approvals",
            Self::Timeline => "Timeline",
            Self::Health => "Health & metrics",
            Self::Errors => "Recent errors",
            Self::Diagnostics => "Diagnostics",
            Self::Preferences => "Preferences",
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PreferenceField {
    Mode,
    LlmEndpoint,
    LlmApiKey,
    LlmModel,
    GridLoginUrl,
    GridAvatarName,
    GridPassword,
    PrivilegedUsers,
}

impl PreferenceField {
    const ALL: [Self; 8] = [
        Self::Mode,
        Self::LlmEndpoint,
        Self::LlmApiKey,
        Self::LlmModel,
        Self::GridLoginUrl,
        Self::GridAvatarName,
        Self::GridPassword,
        Self::PrivilegedUsers,
    ];

    const fn secret(self) -> bool {
        matches!(self, Self::LlmApiKey | Self::GridPassword)
    }
}

pub struct PreferencesPanel {
    path: PathBuf,
    preferences: AgentPreferences,
    selected: usize,
    editing: bool,
    buffer: String,
    status: String,
}

impl fmt::Debug for PreferencesPanel {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("PreferencesPanel")
            .field("path", &self.path)
            .field("selected", &self.selected)
            .field("editing", &self.editing)
            .field("buffer_bytes", &self.buffer.len())
            .finish_non_exhaustive()
    }
}

impl PreferencesPanel {
    pub fn open(path: PathBuf) -> Result<Self, TuiError> {
        let preferences =
            AgentPreferences::load(&path).map_err(|error| TuiError(error.to_string()))?;
        Ok(Self {
            path,
            preferences,
            selected: 0,
            editing: false,
            buffer: String::new(),
            status: "ready".into(),
        })
    }

    #[must_use]
    pub fn summary(&self) -> PreferencesSummary {
        self.preferences.summary()
    }

    #[must_use]
    pub fn selected_field(&self) -> PreferenceField {
        PreferenceField::ALL[self.selected]
    }

    pub fn next_field(&mut self) {
        self.selected = (self.selected + 1) % PreferenceField::ALL.len();
    }
    pub fn previous_field(&mut self) {
        self.selected =
            (self.selected + PreferenceField::ALL.len() - 1) % PreferenceField::ALL.len();
    }

    pub fn begin_edit(&mut self) {
        self.editing = true;
        self.buffer.clear();
        self.status = if self.selected_field().secret() {
            "editing secret (hidden)".into()
        } else {
            "editing".into()
        };
    }

    pub fn push(&mut self, value: char) {
        if self.editing && !value.is_control() && self.buffer.len() < 16 * 1024 {
            self.buffer.push(value);
        }
    }

    pub fn backspace(&mut self) {
        if self.editing {
            self.buffer.pop();
        }
    }

    pub fn cancel_edit(&mut self) {
        self.editing = false;
        self.buffer.clear();
        self.status = "edit cancelled".into();
    }

    pub fn commit_edit(&mut self) -> Result<(), TuiError> {
        if !self.editing {
            return Ok(());
        }
        let value = std::mem::take(&mut self.buffer);
        let summary = self.preferences.summary();
        match self.selected_field() {
            PreferenceField::Mode => {
                let mode = match value.trim().to_ascii_lowercase().as_str() {
                    "offline" => OperatingMode::OfflineFake,
                    "integrated" => OperatingMode::Integrated,
                    "split" => OperatingMode::SplitService,
                    _ => {
                        return Err(TuiError(
                            "mode must be offline, integrated, or split".into(),
                        ));
                    }
                };
                self.preferences.set_mode(mode);
            }
            PreferenceField::LlmEndpoint => self.preferences.set_llm(value, None),
            PreferenceField::LlmApiKey => self
                .preferences
                .set_llm(summary.llm_endpoint.unwrap_or_default(), Some(value)),
            PreferenceField::LlmModel => self.preferences.set_llm_model(value),
            PreferenceField::GridLoginUrl => {
                self.preferences.set_grid(
                    value,
                    summary.grid_avatar_name.unwrap_or_default(),
                    None,
                );
            }
            PreferenceField::GridAvatarName => {
                self.preferences
                    .set_grid(summary.grid_login_url.unwrap_or_default(), value, None);
            }
            PreferenceField::GridPassword => self.preferences.set_grid(
                summary.grid_login_url.unwrap_or_default(),
                summary.grid_avatar_name.unwrap_or_default(),
                Some(value),
            ),
            PreferenceField::PrivilegedUsers => self.preferences.set_privileged_users(
                value
                    .split(',')
                    .map(str::trim)
                    .filter(|item| !item.is_empty())
                    .map(str::to_owned)
                    .collect(),
            ),
        }
        self.editing = false;
        self.status = "changed; press w to save".into();
        Ok(())
    }

    pub fn save(&mut self) -> Result<(), TuiError> {
        self.preferences
            .save(&self.path)
            .map_err(|error| TuiError(error.to_string()))?;
        self.status = "saved; restart service to apply connection changes".into();
        Ok(())
    }

    fn render(&self, out: &mut Vec<String>) {
        let summary = self.summary();
        out.push(format!(
            "file={} mode={:?}",
            self.path.display(),
            summary.mode
        ));
        let values = [
            format!("mode: {:?}", summary.mode),
            format!(
                "LLM endpoint: {}",
                summary.llm_endpoint.as_deref().unwrap_or("<unset>")
            ),
            format!(
                "LLM API key: {}",
                configured(summary.llm_api_key_configured)
            ),
            format!(
                "LLM model: {}",
                summary.llm_model.as_deref().unwrap_or("<endpoint default>")
            ),
            format!(
                "grid login URL: {}",
                summary.grid_login_url.as_deref().unwrap_or("<unset>")
            ),
            format!(
                "grid avatar: {}",
                summary.grid_avatar_name.as_deref().unwrap_or("<unset>")
            ),
            format!(
                "grid password: {}",
                configured(summary.grid_password_configured)
            ),
            format!("privileged users: {}", summary.privileged_users.join(", ")),
        ];
        for (index, value) in values.into_iter().enumerate() {
            out.push(format!(
                "{} {value}",
                if index == self.selected { ">" } else { " " }
            ));
        }
        if self.editing {
            out.push(format!(
                "input: {}",
                if self.selected_field().secret() {
                    "•".repeat(self.buffer.chars().count())
                } else {
                    self.buffer.clone()
                }
            ));
        }
        out.push(format!(
            "{} | [↑/↓] field [Enter] edit/commit [Esc] cancel [w] save",
            self.status
        ));
    }
}

fn configured(value: bool) -> &'static str {
    if value { "[configured]" } else { "[unset]" }
}

#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct EventFilter {
    pub query: String,
    pub severity: Option<EventSeverity>,
    pub component: Option<String>,
    pub avatar: Option<String>,
    pub session: Option<String>,
    pub action: Option<String>,
    pub since_unix_millis: Option<u64>,
}

impl EventFilter {
    fn matches(&self, item: &StructuredEvent) -> bool {
        if self.severity.is_some_and(|value| value != item.severity)
            || self
                .component
                .as_ref()
                .is_some_and(|value| value != &item.component)
            || self
                .since_unix_millis
                .is_some_and(|value| item.unix_millis < value)
            || self
                .avatar
                .as_ref()
                .is_some_and(|value| item.correlation.avatar_id.as_ref() != Some(value))
            || self
                .session
                .as_ref()
                .is_some_and(|value| item.correlation.session_id.as_ref() != Some(value))
            || self
                .action
                .as_ref()
                .is_some_and(|value| item.correlation.action_id.as_ref() != Some(value))
        {
            return false;
        }
        let query = self.query.to_lowercase();
        query.is_empty()
            || item.component.to_lowercase().contains(&query)
            || item.family.to_lowercase().contains(&query)
            || item
                .result_code
                .as_ref()
                .is_some_and(|v| v.to_lowercase().contains(&query))
            || item
                .reason_code
                .as_ref()
                .is_some_and(|v| v.to_lowercase().contains(&query))
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct OperatorSnapshot {
    pub health: Option<HealthView>,
    pub runtime: Option<RuntimeView>,
    pub metrics: Option<MetricsSnapshot>,
    pub sessions: Vec<SessionMetadataView>,
    pub schedules: Vec<ScheduledJobView>,
    pub approvals: Vec<PendingApprovalView>,
    pub audit: Vec<AuditEventView>,
    pub timeline: VecDeque<StructuredEvent>,
    pub gap_count: u64,
    pub last_sequence: Option<u64>,
}

impl Default for OperatorSnapshot {
    fn default() -> Self {
        Self {
            health: None,
            runtime: None,
            metrics: None,
            sessions: Vec::new(),
            schedules: Vec::new(),
            approvals: Vec::new(),
            audit: Vec::new(),
            timeline: VecDeque::with_capacity(MAX_TIMELINE),
            gap_count: 0,
            last_sequence: None,
        }
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum OperatorCommand {
    Pause,
    Resume,
    CancelAction(String),
    DecideApproval { id: u64, approve: bool },
    Reconnect,
    ExpireSession { avatar_id: String, direct_im: bool },
    ToggleSchedule { job_id: String, enabled: bool },
    Shutdown,
}

impl OperatorCommand {
    fn request(&self) -> ControlRequest {
        match self {
            Self::Pause => ControlRequest::PauseAutonomy,
            Self::Resume => ControlRequest::ResumeAutonomy,
            Self::CancelAction(action_id) => ControlRequest::CancelAction {
                action_id: action_id.clone(),
            },
            Self::DecideApproval { id, approve } => ControlRequest::DecideApproval {
                approval_id: *id,
                approve: *approve,
            },
            Self::Reconnect => ControlRequest::ForceReconnect,
            Self::ExpireSession {
                avatar_id,
                direct_im,
            } => ControlRequest::ExpireConversation {
                avatar_id: avatar_id.clone(),
                channel: if *direct_im {
                    crate::control_plane::ConversationChannelView::DirectIm
                } else {
                    crate::control_plane::ConversationChannelView::PublicChat
                },
            },
            Self::ToggleSchedule { job_id, enabled } => ControlRequest::SetRoamingJob {
                job_id: job_id.clone(),
                enabled: *enabled,
            },
            Self::Shutdown => ControlRequest::GracefulShutdown,
        }
    }

    #[must_use]
    pub const fn confirmation(&self) -> CommandConfirmation {
        match self {
            Self::Pause | Self::Resume => CommandConfirmation::None,
            Self::CancelAction(_)
            | Self::DecideApproval { .. }
            | Self::Reconnect
            | Self::ExpireSession { .. }
            | Self::ToggleSchedule { .. } => CommandConfirmation::Normal,
            Self::Shutdown => CommandConfirmation::HighRisk,
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CommandConfirmation {
    None,
    Normal,
    HighRisk,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum TuiInput {
    NextScreen,
    PreviousScreen,
    ScrollUp,
    ScrollDown,
    Refresh,
    Command(OperatorCommand),
    Confirm,
    Reject,
    Quit,
    PreferenceNext,
    PreferencePrevious,
    PreferenceEditOrCommit,
    PreferenceCharacter(char),
    PreferenceBackspace,
    PreferenceSave,
    PreferenceCancel,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum TuiAction {
    None,
    Refresh,
    Execute(OperatorCommand),
    Exit,
}

#[derive(Debug)]
pub struct TuiError(pub String);
impl fmt::Display for TuiError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.0)
    }
}
impl std::error::Error for TuiError {}

pub type TuiFuture<'a> =
    Pin<Box<dyn Future<Output = Result<ControlResponseEnvelope, TuiError>> + Send + 'a>>;

/// Common authenticated client boundary for embedded and split operation.
pub trait TuiTransport: Send + Sync {
    fn request(&self, request: ControlRequestEnvelope) -> TuiFuture<'_>;
}

impl TuiTransport for InProcessControlClient {
    fn request(&self, request: ControlRequestEnvelope) -> TuiFuture<'_> {
        Box::pin(async move { Ok(InProcessControlClient::request(self, request).await) })
    }
}

impl TuiTransport for TcpControlClient {
    fn request(&self, request: ControlRequestEnvelope) -> TuiFuture<'_> {
        Box::pin(async move {
            TcpControlClient::request(self, request)
                .await
                .map_err(|e| TuiError(e.to_string()))
        })
    }
}

/// Loopback split-mode transport which reconnects after a service restart.
/// The capability is held in the redacting secret wrapper and never appears in
/// diagnostics. Requests are retried once only when the transport itself died;
/// application errors are not replayed.
pub struct ReconnectingTcpTransport {
    address: SocketAddr,
    token: SecretString,
    limits: ControlLimits,
    client: tokio::sync::Mutex<Option<TcpControlClient>>,
}

impl ReconnectingTcpTransport {
    #[must_use]
    pub fn new(address: SocketAddr, token: SecretString, limits: ControlLimits) -> Self {
        Self {
            address,
            token,
            limits,
            client: tokio::sync::Mutex::new(None),
        }
    }
}

impl fmt::Debug for ReconnectingTcpTransport {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("ReconnectingTcpTransport")
            .field("address", &self.address)
            .field("token", &"[REDACTED]")
            .field("limits", &self.limits)
            .finish_non_exhaustive()
    }
}

impl TuiTransport for ReconnectingTcpTransport {
    fn request(&self, request: ControlRequestEnvelope) -> TuiFuture<'_> {
        Box::pin(async move {
            let mut client = self.client.lock().await;
            if client.is_none() {
                *client = Some(
                    TcpControlClient::connect(
                        self.address,
                        self.token.expose_secret(),
                        self.limits,
                    )
                    .await
                    .map_err(|error| TuiError(error.to_string()))?,
                );
            }
            let result = client
                .as_ref()
                .expect("client initialized")
                .request(request.clone())
                .await;
            match result {
                Ok(response) => Ok(response),
                Err(error) if error.retryable => {
                    *client = None;
                    let replacement = TcpControlClient::connect(
                        self.address,
                        self.token.expose_secret(),
                        self.limits,
                    )
                    .await
                    .map_err(|connect| TuiError(connect.to_string()))?;
                    let response = replacement
                        .request(request)
                        .await
                        .map_err(|retry| TuiError(retry.to_string()))?;
                    *client = Some(replacement);
                    Ok(response)
                }
                Err(error) => Err(TuiError(error.to_string())),
            }
        })
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TuiRenderOptions {
    pub width: u16,
    pub height: u16,
    pub color: bool,
}

pub struct OperatorTui {
    pub snapshot: OperatorSnapshot,
    pub screen: OperatorScreen,
    pub filter: EventFilter,
    pending: Option<OperatorCommand>,
    request_number: u64,
    scroll: usize,
    status: String,
    errors: VecDeque<String>,
    preferences: Option<PreferencesPanel>,
}

impl Default for OperatorTui {
    fn default() -> Self {
        Self {
            snapshot: OperatorSnapshot::default(),
            screen: OperatorScreen::Overview,
            filter: EventFilter::default(),
            pending: None,
            request_number: 1,
            scroll: 0,
            status: "connecting".into(),
            errors: VecDeque::with_capacity(MAX_ERRORS),
            preferences: None,
        }
    }
}

impl OperatorTui {
    pub fn with_preferences(path: PathBuf) -> Result<Self, TuiError> {
        Ok(Self {
            preferences: Some(PreferencesPanel::open(path)?),
            ..Self::default()
        })
    }

    #[must_use]
    pub fn command_shortcut(&self, key: char) -> Option<OperatorCommand> {
        match key {
            'p' => Some(OperatorCommand::Pause),
            'u' => Some(OperatorCommand::Resume),
            'f' => Some(OperatorCommand::Reconnect),
            'a' | 'd' => {
                self.snapshot
                    .approvals
                    .first()
                    .map(|approval| OperatorCommand::DecideApproval {
                        id: approval.approval_id,
                        approve: key == 'a',
                    })
            }
            'x' => self.snapshot.timeline.iter().rev().find_map(|event| {
                event
                    .correlation
                    .action_id
                    .clone()
                    .map(OperatorCommand::CancelAction)
            }),
            'e' => self
                .snapshot
                .sessions
                .first()
                .map(|session| OperatorCommand::ExpireSession {
                    avatar_id: session.avatar_id.clone(),
                    direct_im: session.channel == "direct_im",
                }),
            't' => self
                .snapshot
                .schedules
                .first()
                .map(|job| OperatorCommand::ToggleSchedule {
                    job_id: job.job_id.clone(),
                    enabled: !job.enabled,
                }),
            's' => Some(OperatorCommand::Shutdown),
            _ => None,
        }
    }

    pub fn reduce(&mut self, input: TuiInput) -> TuiAction {
        match input {
            TuiInput::NextScreen => {
                let i = OperatorScreen::ALL
                    .iter()
                    .position(|v| *v == self.screen)
                    .unwrap_or(0);
                self.screen = OperatorScreen::ALL[(i + 1) % OperatorScreen::ALL.len()];
                self.scroll = 0;
                TuiAction::None
            }
            TuiInput::PreviousScreen => {
                let i = OperatorScreen::ALL
                    .iter()
                    .position(|v| *v == self.screen)
                    .unwrap_or(0);
                self.screen = OperatorScreen::ALL
                    [(i + OperatorScreen::ALL.len() - 1) % OperatorScreen::ALL.len()];
                self.scroll = 0;
                TuiAction::None
            }
            TuiInput::ScrollUp => {
                self.scroll = self.scroll.saturating_sub(1);
                TuiAction::None
            }
            TuiInput::ScrollDown => {
                self.scroll = self.scroll.saturating_add(1);
                TuiAction::None
            }
            TuiInput::Refresh => TuiAction::Refresh,
            TuiInput::Quit => TuiAction::Exit,
            TuiInput::PreferenceNext => {
                if let Some(panel) = &mut self.preferences {
                    panel.next_field();
                }
                TuiAction::None
            }
            TuiInput::PreferencePrevious => {
                if let Some(panel) = &mut self.preferences {
                    panel.previous_field();
                }
                TuiAction::None
            }
            TuiInput::PreferenceEditOrCommit => {
                if let Some(panel) = &mut self.preferences {
                    if panel.editing {
                        if let Err(error) = panel.commit_edit() {
                            self.record_error(error.to_string());
                        }
                    } else {
                        panel.begin_edit();
                    }
                }
                TuiAction::None
            }
            TuiInput::PreferenceCharacter(value) => {
                if let Some(panel) = &mut self.preferences {
                    panel.push(value);
                }
                TuiAction::None
            }
            TuiInput::PreferenceBackspace => {
                if let Some(panel) = &mut self.preferences {
                    panel.backspace();
                }
                TuiAction::None
            }
            TuiInput::PreferenceSave => {
                if let Some(panel) = &mut self.preferences
                    && let Err(error) = panel.save()
                {
                    self.record_error(error.to_string());
                }
                TuiAction::None
            }
            TuiInput::PreferenceCancel => {
                if let Some(panel) = &mut self.preferences {
                    panel.cancel_edit();
                }
                TuiAction::None
            }
            TuiInput::Command(command) if command.confirmation() == CommandConfirmation::None => {
                TuiAction::Execute(command)
            }
            TuiInput::Command(command) => {
                self.status = format!("confirm {:?} (y/n)", command.confirmation());
                self.pending = Some(command);
                TuiAction::None
            }
            TuiInput::Confirm => self
                .pending
                .take()
                .map_or(TuiAction::None, TuiAction::Execute),
            TuiInput::Reject => {
                self.pending = None;
                self.status = "command cancelled".into();
                TuiAction::None
            }
        }
    }

    pub fn ingest_event(&mut self, event: ControlEvent) {
        self.snapshot.last_sequence = Some(event.sequence);
        match event.event {
            ControlEventKind::Gap {
                first_available,
                last_missed,
            } => {
                self.snapshot.gap_count = self.snapshot.gap_count.saturating_add(
                    last_missed
                        .saturating_sub(first_available)
                        .saturating_add(1),
                );
                self.status = format!("event gap before {first_available}");
            }
            ControlEventKind::Mutation {
                operation, outcome, ..
            } => self.status = format!("{operation}: {outcome}"),
            ControlEventKind::StateChanged { component, state } => {
                self.status = format!("{component}: {state}");
            }
        }
    }

    pub async fn refresh(&mut self, client: &dyn TuiTransport) -> Result<(), TuiError> {
        let requests = [
            ControlRequest::Health,
            ControlRequest::Runtime,
            ControlRequest::Metrics,
            ControlRequest::ListSessions { page: page() },
            ControlRequest::ListScheduledJobs { page: page() },
            ControlRequest::ListPendingApprovals { page: page() },
            ControlRequest::ListAuditEvents { page: page() },
            ControlRequest::ListObservabilityEvents { page: page() },
        ];
        for request in requests {
            self.send_and_apply(client, request).await?;
        }
        self.status = "connected".into();
        Ok(())
    }

    pub async fn execute(
        &mut self,
        client: &dyn TuiTransport,
        command: OperatorCommand,
    ) -> Result<(), TuiError> {
        self.send_and_apply(client, command.request()).await?;
        self.refresh(client).await
    }

    async fn send_and_apply(
        &mut self,
        client: &dyn TuiTransport,
        request: ControlRequest,
    ) -> Result<(), TuiError> {
        let envelope = ControlRequestEnvelope {
            version: CONTROL_PROTOCOL_VERSION,
            request_id: format!("tui-{}", self.request_number),
            request,
        };
        self.request_number = self.request_number.saturating_add(1);
        let response = client.request(envelope).await?;
        match response.result {
            Ok(payload) => self.apply(payload),
            Err(error) => {
                let message = format!("{:?}: {}", error.code, error.message);
                self.record_error(message.clone());
                return Err(TuiError(message));
            }
        }
        Ok(())
    }

    fn apply(&mut self, payload: ControlPayload) {
        match payload {
            ControlPayload::Health(value) => self.snapshot.health = Some(value),
            ControlPayload::Metrics(value) => self.snapshot.metrics = Some(value),
            ControlPayload::Runtime(value) => self.snapshot.runtime = Some(value),
            ControlPayload::Sessions(Page { items, .. }) => self.snapshot.sessions = items,
            ControlPayload::ScheduledJobs(Page { items, .. }) => self.snapshot.schedules = items,
            ControlPayload::PendingApprovals(Page { items, .. }) => self.snapshot.approvals = items,
            ControlPayload::AuditEvents(Page { items, .. }) => self.snapshot.audit = items,
            ControlPayload::ObservabilityEvents(Page { items, .. }) => {
                for event in items {
                    if self.snapshot.timeline.len() == MAX_TIMELINE {
                        self.snapshot.timeline.pop_front();
                    }
                    self.snapshot.timeline.push_back(event);
                }
            }
            ControlPayload::Accepted { operation_id } => {
                self.status = format!("accepted {operation_id}");
            }
            ControlPayload::Completed => self.status = "completed".into(),
            ControlPayload::Subscribed { current_sequence } => {
                self.snapshot.last_sequence = Some(current_sequence);
            }
            ControlPayload::Cancelled { request_id } => {
                self.status = format!("cancelled {request_id}");
            }
        }
    }

    fn record_error(&mut self, message: String) {
        if self.errors.len() == MAX_ERRORS {
            self.errors.pop_front();
        }
        self.errors.push_back(message);
    }

    #[must_use]
    #[allow(clippy::too_many_lines)]
    pub fn render(&self, options: TuiRenderOptions) -> String {
        let width = usize::from(options.width.max(20));
        let height = usize::from(options.height.max(5));
        let mut lines = vec![
            format!(
                "MetaCrate agent | {} | {}",
                self.screen.title(),
                self.status
            ),
            format!(
                "[Tab] next [r] refresh [p] pause [u] resume [q] quit | gaps {}",
                self.snapshot.gap_count
            ),
        ];
        match self.screen {
            OperatorScreen::Overview => render_overview(&self.snapshot, &mut lines),
            OperatorScreen::Sessions => {
                lines.push("active sessions (metadata only)".into());
                for v in &self.snapshot.sessions {
                    lines.push(format!(
                        "{} {} {} turns={} bytes={}",
                        v.session_id, v.avatar_id, v.channel, v.turns, v.bytes
                    ));
                }
            }
            OperatorScreen::QueuesAndBudgets => render_queues(&self.snapshot, &mut lines),
            OperatorScreen::Roaming => {
                for v in &self.snapshot.schedules {
                    lines.push(format!(
                        "{} {} enabled={} next={:?}",
                        v.job_id, v.kind, v.enabled, v.next_run_unix_millis
                    ));
                }
            }
            OperatorScreen::Approvals => {
                for v in &self.snapshot.approvals {
                    lines.push(format!(
                        "#{} tool={} principal={} expires={}",
                        v.approval_id, v.tool, v.principal, v.expires_unix_seconds
                    ));
                }
            }
            OperatorScreen::Timeline => {
                lines.push(format!("filter={:?}", self.filter));
                for v in self
                    .snapshot
                    .timeline
                    .iter()
                    .filter(|v| self.filter.matches(v))
                {
                    lines.push(format!(
                        "{} {:?} {} {} result={:?}",
                        v.unix_millis, v.severity, v.component, v.family, v.result_code
                    ));
                }
            }
            OperatorScreen::Health => render_health(&self.snapshot, &mut lines),
            OperatorScreen::Errors => {
                for v in &self.errors {
                    lines.push(v.clone());
                }
                for v in self
                    .snapshot
                    .timeline
                    .iter()
                    .filter(|v| v.severity == EventSeverity::Error)
                {
                    lines.push(format!("{} {} {}", v.unix_millis, v.component, v.family));
                }
            }
            OperatorScreen::Diagnostics => {
                lines.push("redacted diagnostic envelopes; content is never retained".into());
                for v in self
                    .snapshot
                    .timeline
                    .iter()
                    .filter(|v| v.family == "diagnostic_envelope")
                {
                    lines.push(format!(
                        "{} {} flags={}",
                        v.unix_millis,
                        v.component,
                        v.redaction_flags.join(",")
                    ));
                }
            }
            OperatorScreen::Preferences => {
                if let Some(panel) = &self.preferences {
                    panel.render(&mut lines);
                } else {
                    lines.push("preferences unavailable: no local config path".into());
                }
            }
        }
        let body_height = height.saturating_sub(1);
        lines
            .into_iter()
            .skip(self.scroll.min(body_height))
            .take(body_height)
            .map(|line| truncate_width(&line, width))
            .collect::<Vec<_>>()
            .join("\n")
    }
}

fn page() -> PageRequest {
    PageRequest {
        cursor: None,
        limit: PAGE_LIMIT,
    }
}
fn render_overview(s: &OperatorSnapshot, out: &mut Vec<String>) {
    if let Some(v) = &s.health {
        out.push(format!(
            "service={} ready={} uptime={}s",
            v.service_state, v.ready, v.uptime_seconds
        ));
    }
    if let Some(v) = &s.runtime {
        out.push(format!(
            "grid={} generation={} connected={} ready={}",
            v.grid_state, v.generation, v.transport_connected, v.agent_ready
        ));
        out.push(format!(
            "region={} pose={:?} behavior={}",
            v.region_name.as_deref().unwrap_or("unknown"),
            v.position,
            v.behavior_mode
        ));
        if v.active_build_transaction.is_some() || !v.build_orphan_ids.is_empty() {
            out.push(format!(
                "build={} progress={} recoverable-orphans={}",
                v.active_build_transaction.as_deref().unwrap_or("idle"),
                v.build_progress.as_deref().unwrap_or("none"),
                v.build_orphan_ids.len()
            ));
        }
        if v.active_visual_capture.is_some() || v.visual_image_sha256.is_some() {
            out.push(format!(
                "vision={} progress={} last-image-sha256={}",
                v.active_visual_capture.as_deref().unwrap_or("idle"),
                v.visual_progress.as_deref().unwrap_or("none"),
                v.visual_image_sha256.as_deref().unwrap_or("none")
            ));
        }
    }
}
fn render_queues(s: &OperatorSnapshot, out: &mut Vec<String>) {
    if let Some(v) = &s.runtime {
        out.push(format!(
            "control queue {}/{}",
            v.control_queue_used, v.control_queue_capacity
        ));
        out.push(format!(
            "tool calls={} movement-mm={}",
            v.budget_tool_calls_used, v.budget_movement_millimeters_used
        ));
    }
    if let Some(v) = &s.metrics {
        out.push(format!(
            "work queue {}/{} tasks={} rate {}/{}",
            v.queue_depth,
            v.queue_capacity,
            v.active_tasks,
            v.rate_limit_used,
            v.rate_limit_capacity
        ));
    }
}
fn render_health(s: &OperatorSnapshot, out: &mut Vec<String>) {
    render_overview(s, out);
    if let Some(v) = &s.metrics {
        out.push(format!(
            "sessions={} reconnects={} events={}",
            v.active_sessions, v.reconnects, v.recorded_events
        ));
        out.push(format!(
            "dropped ring={} subscriber={} journal={}",
            v.dropped_ring_events, v.dropped_subscriber_events, v.dropped_journal_events
        ));
    }
}

fn truncate_width(value: &str, width: usize) -> String {
    if UnicodeWidthStr::width(value) <= width {
        return value.to_owned();
    }
    let mut result = String::new();
    let mut used = 0;
    for ch in value.chars() {
        let next = ch.width().unwrap_or(0);
        if used + next + 1 > width {
            break;
        }
        result.push(ch);
        used += next;
    }
    result.push('…');
    result
}

struct TerminalGuard;
impl TerminalGuard {
    fn enter() -> io::Result<Self> {
        terminal::enable_raw_mode()?;
        execute!(io::stdout(), terminal::EnterAlternateScreen, cursor::Hide)?;
        Ok(Self)
    }
}
impl Drop for TerminalGuard {
    fn drop(&mut self) {
        let _ = execute!(io::stdout(), cursor::Show, terminal::LeaveAlternateScreen);
        let _ = terminal::disable_raw_mode();
    }
}

/// Opens the local configuration preferences panel without starting or
/// connecting to the agent service. Secret fields are rendered as bullets.
pub fn run_preferences_terminal(path: PathBuf, _color: bool) -> Result<(), TuiError> {
    let _guard = TerminalGuard::enter().map_err(|error| TuiError(error.to_string()))?;
    let mut panel = PreferencesPanel::open(path)?;
    loop {
        let (width, height) = terminal::size().unwrap_or((80, 24));
        let mut lines = vec!["MetaCrate agent | Preferences".to_owned()];
        panel.render(&mut lines);
        let text = lines
            .into_iter()
            .take(usize::from(height.max(5)))
            .map(|line| truncate_width(&line, usize::from(width.max(20))))
            .collect::<Vec<_>>()
            .join("\n");
        let mut stdout = io::stdout();
        queue!(
            stdout,
            cursor::MoveTo(0, 0),
            terminal::Clear(terminal::ClearType::All),
            style::Print(text)
        )
        .map_err(|error| TuiError(error.to_string()))?;
        stdout
            .flush()
            .map_err(|error| TuiError(error.to_string()))?;
        let event::Event::Key(key) = event::read().map_err(|error| TuiError(error.to_string()))?
        else {
            continue;
        };
        if key.kind != event::KeyEventKind::Press {
            continue;
        }
        if panel.editing {
            match key.code {
                event::KeyCode::Enter => panel.commit_edit()?,
                event::KeyCode::Backspace => panel.backspace(),
                event::KeyCode::Esc => panel.cancel_edit(),
                event::KeyCode::Char(value) => panel.push(value),
                _ => {}
            }
        } else {
            match key.code {
                event::KeyCode::Up => panel.previous_field(),
                event::KeyCode::Down => panel.next_field(),
                event::KeyCode::Enter => panel.begin_edit(),
                event::KeyCode::Char('w') => panel.save()?,
                event::KeyCode::Char('q') | event::KeyCode::Esc => return Ok(()),
                event::KeyCode::Char('c')
                    if key.modifiers.contains(event::KeyModifiers::CONTROL) =>
                {
                    return Ok(());
                }
                _ => {}
            }
        }
    }
}

/// Runs an event-driven keyboard UI. Terminal state is restored on normal
/// return, errors, Ctrl-C, and unwinding because restoration is guard-owned.
pub async fn run_terminal(client: Arc<dyn TuiTransport>, color: bool) -> Result<(), TuiError> {
    run_terminal_with_preferences(client, color, None).await
}

#[allow(clippy::too_many_lines)]
pub async fn run_terminal_with_preferences(
    client: Arc<dyn TuiTransport>,
    color: bool,
    preferences_path: Option<PathBuf>,
) -> Result<(), TuiError> {
    let _guard = TerminalGuard::enter().map_err(|e| TuiError(e.to_string()))?;
    let (input_tx, mut input_rx) = tokio::sync::mpsc::channel(64);
    std::thread::spawn(move || {
        while let Ok(value) = event::read() {
            if input_tx.blocking_send(value).is_err() {
                break;
            }
        }
    });
    let mut app = if let Some(path) = preferences_path {
        OperatorTui::with_preferences(path)?
    } else {
        OperatorTui::default()
    };
    app.refresh(client.as_ref()).await?;
    let mut refresh = tokio::time::interval(Duration::from_secs(1));
    refresh.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
    refresh.tick().await;
    loop {
        let (width, height) = terminal::size().unwrap_or((80, 24));
        let text = app.render(TuiRenderOptions {
            width,
            height,
            color,
        });
        let mut stdout = io::stdout();
        queue!(
            stdout,
            cursor::MoveTo(0, 0),
            terminal::Clear(terminal::ClearType::All),
            style::Print(text)
        )
        .map_err(|e| TuiError(e.to_string()))?;
        stdout.flush().map_err(|e| TuiError(e.to_string()))?;
        let value = tokio::select! {
            value = input_rx.recv() => {
                let Some(value) = value else { return Ok(()); };
                value
            }
            _ = refresh.tick() => {
                // One coalesced snapshot replaces any number of service changes;
                // skipped ticks prevent a slow service from building refresh work.
                if let Err(error) = app.refresh(client.as_ref()).await {
                    app.record_error(error.to_string());
                }
                continue;
            }
        };
        let input = match value {
            event::Event::Resize(_, _) => TuiInput::Refresh,
            event::Event::Key(key)
                if key.kind == event::KeyEventKind::Press
                    && app.screen == OperatorScreen::Preferences
                    && app.preferences.as_ref().is_some_and(|panel| panel.editing) =>
            {
                match key.code {
                    event::KeyCode::Enter => TuiInput::PreferenceEditOrCommit,
                    event::KeyCode::Backspace => TuiInput::PreferenceBackspace,
                    event::KeyCode::Esc => TuiInput::PreferenceCancel,
                    event::KeyCode::Char(value) => TuiInput::PreferenceCharacter(value),
                    _ => continue,
                }
            }
            event::Event::Key(key)
                if key.kind == event::KeyEventKind::Press
                    && app.screen == OperatorScreen::Preferences =>
            {
                match key.code {
                    event::KeyCode::Up => TuiInput::PreferencePrevious,
                    event::KeyCode::Down => TuiInput::PreferenceNext,
                    event::KeyCode::Enter => TuiInput::PreferenceEditOrCommit,
                    event::KeyCode::Char('w') => TuiInput::PreferenceSave,
                    event::KeyCode::Esc => TuiInput::PreferenceCancel,
                    event::KeyCode::Tab | event::KeyCode::Right => TuiInput::NextScreen,
                    event::KeyCode::BackTab | event::KeyCode::Left => TuiInput::PreviousScreen,
                    event::KeyCode::Char('q') => TuiInput::Quit,
                    _ => continue,
                }
            }
            event::Event::Key(key) if key.kind == event::KeyEventKind::Press => match key.code {
                event::KeyCode::Tab | event::KeyCode::Right => TuiInput::NextScreen,
                event::KeyCode::BackTab | event::KeyCode::Left => TuiInput::PreviousScreen,
                event::KeyCode::Up => TuiInput::ScrollUp,
                event::KeyCode::Down => TuiInput::ScrollDown,
                event::KeyCode::Char('r') => TuiInput::Refresh,
                event::KeyCode::Char(
                    key @ ('p' | 'u' | 'f' | 'a' | 'd' | 'x' | 'e' | 't' | 's'),
                ) => {
                    let Some(command) = app.command_shortcut(key) else {
                        continue;
                    };
                    TuiInput::Command(command)
                }
                event::KeyCode::Char('y') => TuiInput::Confirm,
                event::KeyCode::Char('n') | event::KeyCode::Esc => TuiInput::Reject,
                event::KeyCode::Char('q') => TuiInput::Quit,
                event::KeyCode::Char('c')
                    if key.modifiers.contains(event::KeyModifiers::CONTROL) =>
                {
                    TuiInput::Quit
                }
                _ => continue,
            },
            _ => continue,
        };
        match app.reduce(input) {
            TuiAction::None => {}
            TuiAction::Exit => return Ok(()),
            TuiAction::Refresh => app.refresh(client.as_ref()).await?,
            TuiAction::Execute(command) => app.execute(client.as_ref(), command).await?,
        }
    }
}
