//! TUI do `sdd` (T-01..T-25): cockpit determinístico do fluxo SDD no terminal.
//! Entrada via `sdd tui`. Sem IA, sem rede — orquestra subcomandos `sdd`.

pub mod app;
pub mod components;
pub mod editor;
pub mod event;
pub mod execution;
pub mod loader;
pub mod markdown;
pub mod orchestration;
pub mod palette;
pub mod parallel;
pub mod persist;
pub mod runner;
pub mod stage;
pub mod theme;
pub mod ui;

use anyhow::{bail, Result};
use clap::Args;
use crossterm::event::{self as cevent, Event, KeyEventKind};
use crossterm::execute;
use crossterm::terminal::{
    disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
};
use crossterm::tty::IsTty;
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;
use serde_json::json;
use std::io::{self, Stdout};
use std::path::PathBuf;
use std::sync::mpsc::TryRecvError;
use std::time::Duration;

use app::{App, Screen};
use event::map_key;

/// Caminho para um executável que ignora os argumentos e sai com código 0,
/// usado como stand-in determinístico do binário `sdd` nos testes que spawnam
/// um subprocesso. Cross-platform: `/usr/bin/true` no Unix; um `.cmd` no-op em
/// `%TEMP%` no Windows (gravado de forma idempotente).
#[cfg(test)]
pub(crate) fn noop_success_exe() -> PathBuf {
    #[cfg(windows)]
    {
        use std::io::Write;
        let path = std::env::temp_dir().join("sdd-test-noop.cmd");
        if let Ok(mut file) = std::fs::File::create(&path) {
            let _ = file.write_all(b"@exit /b 0\r\n");
        }
        path
    }
    #[cfg(not(windows))]
    {
        PathBuf::from("/usr/bin/true")
    }
}

#[derive(Args)]
pub struct TuiArgs {
    /// Raiz do projeto (default: diretório corrente).
    #[arg(long)]
    pub root: Option<PathBuf>,
}

/// Decide se o ambiente suporta TUI. Testável sem terminal real (RF-11/CA-RF11).
pub fn tty_ready(is_tty: bool, term: Option<&str>) -> bool {
    if !is_tty {
        return false;
    }
    !matches!(term, Some("dumb"))
}

/// Restaura o terminal mesmo em panic (RT-01).
struct TerminalGuard;

impl TerminalGuard {
    fn enter() -> Result<TerminalGuard> {
        enable_raw_mode()?;
        execute!(io::stdout(), EnterAlternateScreen)?;
        Ok(TerminalGuard)
    }
}

impl Drop for TerminalGuard {
    fn drop(&mut self) {
        let _ = disable_raw_mode();
        let _ = execute!(io::stdout(), LeaveAlternateScreen);
    }
}

fn install_panic_hook() {
    let original = std::panic::take_hook();
    std::panic::set_hook(Box::new(move |info| {
        let _ = disable_raw_mode();
        let _ = execute!(io::stdout(), LeaveAlternateScreen);
        original(info);
    }));
}

fn log_event(root: &std::path::Path, event: serde_json::Value) {
    let _ = crate::log_jsonl(root, "tui-events.jsonl", event);
}

pub fn run(args: TuiArgs) -> Result<()> {
    let term = std::env::var("TERM").ok();
    if !tty_ready(io::stdout().is_tty(), term.as_deref()) {
        bail!(
            "sdd tui requer um terminal interativo (TTY). \
             Use os subcomandos individuais: sdd idea, sdd prd, sdd techspec, ..."
        );
    }

    let root = args.root.unwrap_or_else(|| PathBuf::from("."));
    let exe = runner::self_exe();

    install_panic_hook();
    let _guard = TerminalGuard::enter()?;
    let backend = CrosstermBackend::new(io::stdout());
    let mut terminal: Terminal<CrosstermBackend<Stdout>> = Terminal::new(backend)?;

    let mut app = App::new(root.clone(), exe);
    log_event(
        &root,
        json!({"ts": crate::now_public(), "event": "tui_open"}),
    );

    let res = event_loop(&mut terminal, &mut app, &root);

    log_event(
        &root,
        json!({"ts": crate::now_public(), "event": "tui_close"}),
    );
    res
}

fn event_loop(
    terminal: &mut Terminal<CrosstermBackend<Stdout>>,
    app: &mut App,
    root: &std::path::Path,
) -> Result<()> {
    loop {
        if app.needs_redraw {
            terminal.clear()?;
            app.needs_redraw = false;
        }
        terminal.draw(|f| ui::render(f, app))?;

        // Eventos do subprocesso de etapa (não-bloqueante).
        let mut events = Vec::new();
        let mut finished = Vec::new();
        if let Some(rx) = &app.pending {
            while let Ok(event) = rx.try_recv() {
                let is_finished = matches!(event, runner::StageEvent::Finished(_));
                events.push(event);
                if is_finished {
                    break;
                }
            }
        }
        for event in events {
            if let Some(result) = app.on_stage_event(event) {
                finished.push(result);
            }
        }
        for result in finished {
            log_event(
                root,
                json!({
                    "ts": crate::now_public(),
                    "event": "stage_finished",
                    "stage": result.stage.key(),
                    "exit_code": result.exit_code,
                    "duration_ms": result.duration_ms,
                    "agent": result.agent.clone(),
                    "model": result.model.clone(),
                    "usage": {
                        "input_tokens": result.usage.input_tokens_estimate,
                        "cached_input_tokens": result.usage.cached_input_tokens_estimate,
                        "output_tokens": result.usage.output_tokens_estimate,
                        "reasoning_output_tokens": result.usage.reasoning_output_tokens_estimate,
                        "total_tokens": result.usage.total_tokens_estimate,
                        "provider_reported": result.usage.provider_reported,
                        "cost": result.usage.cost.as_ref().map(|cost| json!({
                            "currency": cost.currency,
                            "input_cost": cost.input_cost,
                            "cached_input_cost": cost.cached_input_cost,
                            "output_cost": cost.output_cost,
                            "total_cost": cost.total_cost,
                        })),
                    },
                    "context": {
                        "prompt_tokens_estimate": result.context.prompt_tokens_estimate,
                        "prompt_bytes": result.context.prompt_bytes,
                        "artifact_count": result.context.artifact_count,
                        "artifact_chars": result.context.artifact_chars,
                        "truncated": result.context.truncated,
                        "context_window_tokens": result.context.context_window_tokens,
                    },
                }),
            );
            app.on_stage_result(result);
        }
        for result in drain_execution_events(app) {
            log_event(
                root,
                json!({
                    "ts": crate::now_public(),
                    "event": "execution_task_finished",
                    "stage": result.stage.key(),
                    "exit_code": result.exit_code,
                    "duration_ms": result.duration_ms,
                    "agent": result.agent.clone(),
                    "model": result.model.clone(),
                    "usage": {
                        "input_tokens": result.usage.input_tokens_estimate,
                        "cached_input_tokens": result.usage.cached_input_tokens_estimate,
                        "output_tokens": result.usage.output_tokens_estimate,
                        "reasoning_output_tokens": result.usage.reasoning_output_tokens_estimate,
                        "total_tokens": result.usage.total_tokens_estimate,
                        "provider_reported": result.usage.provider_reported,
                    },
                }),
            );
        }
        for (task_id, result) in drain_parallel_events(app) {
            log_event(
                root,
                json!({
                    "ts": crate::now_public(),
                    "event": "parallel_slot_finished",
                    "stage": result.stage.key(),
                    "task_id": task_id,
                    "exit_code": result.exit_code,
                    "duration_ms": result.duration_ms,
                    "agent": result.agent.clone(),
                    "model": result.model.clone(),
                    "usage": {
                        "input_tokens": result.usage.input_tokens_estimate,
                        "cached_input_tokens": result.usage.cached_input_tokens_estimate,
                        "output_tokens": result.usage.output_tokens_estimate,
                        "reasoning_output_tokens": result.usage.reasoning_output_tokens_estimate,
                        "total_tokens": result.usage.total_tokens_estimate,
                        "provider_reported": result.usage.provider_reported,
                    },
                }),
            );
        }
        if app.screen == Screen::ParallelRunning && app.parallel_barrier_reached() {
            let mut fx = app::SideEffects::default();
            app.resolve_parallel_barrier(&mut fx);
            process_side_effects(root, &fx);
        }
        app.loader.tick();

        if cevent::poll(Duration::from_millis(100))? {
            match cevent::read()? {
                Event::Key(key) if key.kind == KeyEventKind::Press => {
                    let action = map_key(key, app.text_mode());
                    let fx = app.update(action);
                    process_side_effects(root, &fx);
                }
                Event::Resize(_, _) => { /* redraw no próximo ciclo */ }
                _ => {}
            }
        }

        if app.should_quit {
            break;
        }
    }
    Ok(())
}

fn drain_execution_events(app: &mut App) -> Vec<runner::StageResult> {
    let mut finished = Vec::new();
    loop {
        let event = {
            let Some(session) = app.execution.as_ref() else {
                break;
            };
            let Some(rx) = session.rx.as_ref() else {
                break;
            };
            rx.try_recv()
        };
        match event {
            Ok(event) => {
                if let Some(result) = app.on_execution_event(event) {
                    finished.push(result);
                    break;
                }
            }
            Err(TryRecvError::Empty) => break,
            Err(TryRecvError::Disconnected) => {
                app.on_execution_disconnected();
                break;
            }
        }
    }
    finished
}

fn drain_parallel_events(app: &mut App) -> Vec<(String, runner::StageResult)> {
    let mut finished = Vec::new();
    let mut disconnected = Vec::new();
    let len = app.parallel.len();
    for idx in 0..len {
        let task_id = app.parallel[idx].task_id.clone();
        loop {
            let event = app.parallel[idx].rx.try_recv();
            match event {
                Ok(event) => {
                    let result = match &event {
                        runner::StageEvent::Finished(result) => Some(result.clone()),
                        _ => None,
                    };
                    let is_finished = result.is_some();
                    app.on_parallel_event(idx, event);
                    if let Some(result) = result {
                        finished.push((task_id.clone(), result));
                    }
                    if is_finished {
                        break;
                    }
                }
                Err(TryRecvError::Empty) => break,
                Err(TryRecvError::Disconnected) => {
                    disconnected.push(idx);
                    break;
                }
            }
        }
    }
    for idx in disconnected {
        app.on_parallel_disconnected(idx);
    }
    finished
}

fn process_side_effects(root: &std::path::Path, fx: &app::SideEffects) {
    if let Some(stage) = fx.log_stage_started {
        log_event(
            root,
            json!({"ts": crate::now_public(), "event": "stage_started", "stage": stage.key()}),
        );
    }
    if let Some((stage, slot_count)) = fx.log_parallel_started {
        log_event(
            root,
            json!({
                "ts": crate::now_public(),
                "event": "parallel_started",
                "stage": stage.key(),
                "slot_count": slot_count,
            }),
        );
    }
    if let Some((stage, done, errors)) = fx.log_parallel_finished {
        log_event(
            root,
            json!({
                "ts": crate::now_public(),
                "event": "parallel_finished",
                "stage": stage.key(),
                "done": done,
                "errors": errors,
            }),
        );
    }
    if let Some((stage, decision)) = fx.log_checkpoint {
        log_event(
            root,
            json!({
                "ts": crate::now_public(),
                "event": "checkpoint_decision",
                "stage": stage.key(),
                "decision": decision.as_str(),
            }),
        );
    }
    if let Some(slug) = &fx.log_orchestration_selected {
        log_event(
            root,
            json!({"ts": crate::now_public(), "event": "orchestration_selected", "slug": slug}),
        );
    }
    if let Some(provider) = &fx.log_provider_selected {
        log_event(
            root,
            json!({
                "ts": crate::now_public(),
                "event": "provider_selected",
                "provider": provider.id.as_str(),
                "kind": provider.kind.as_str(),
                "model": provider.model.as_str(),
                "effort": provider.effort.as_str(),
                "offline": provider.offline,
                "enabled": provider.enabled,
                "auth_env": provider.auth_env.as_deref(),
                "auth_present": provider.auth_present,
                "auth_methods": &provider.auth_methods,
                "models": &provider.models,
                "efforts": &provider.efforts,
                "stage_models": &provider.stage_models,
                "stage_efforts": &provider.stage_efforts,
                "token_budget": provider.token_budget,
                "usage_limits": &provider.usage_limits,
                "capabilities": &provider.capabilities,
            }),
        );
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::tui::parallel::{AgentHandle, AgentSlotStatus};
    use crate::tui::runner::{StageContextSample, StageEvent, StageResult, TokenUsageSample};
    use crate::tui::stage::Stage;
    use std::sync::mpsc;

    #[test]
    fn degrades_without_tty() {
        assert!(!tty_ready(false, Some("xterm-256color")));
    }

    #[test]
    fn degrades_on_dumb_term() {
        assert!(!tty_ready(true, Some("dumb")));
    }

    #[test]
    fn ready_on_normal_tty() {
        assert!(tty_ready(true, Some("xterm-256color")));
        assert!(tty_ready(true, None));
    }

    fn handle(id: &str, rx: mpsc::Receiver<StageEvent>) -> AgentHandle {
        AgentHandle {
            task_id: id.to_string(),
            task_label: format!("Task {id}"),
            rx,
            status: AgentSlotStatus::Aguardando,
            trace: Vec::new(),
            usage: None,
            result: None,
            fragment_path: PathBuf::from(format!("/tmp/{id}.md")),
        }
    }

    fn result(exit_code: i32) -> StageResult {
        StageResult {
            stage: Stage::Execution,
            exit_code,
            stdout: String::new(),
            stderr: String::new(),
            duration_ms: 1,
            trace: vec!["done".to_string()],
            agent: runner::AgentProvenance::default(),
            model: runner::ModelProvenance::default(),
            usage: TokenUsageSample::from_text("in", "out"),
            context: StageContextSample::default(),
        }
    }

    #[test]
    fn drains_parallel_events_for_all_slots() {
        let mut app = App::new(PathBuf::from("."), PathBuf::from("sdd"));
        let (tx1, rx1) = mpsc::channel();
        let (tx2, rx2) = mpsc::channel();
        tx1.send(StageEvent::Trace("slot 1".to_string())).unwrap();
        tx1.send(StageEvent::Finished(result(0))).unwrap();
        tx2.send(StageEvent::Finished(result(1))).unwrap();
        app.parallel = vec![handle("T-01", rx1), handle("T-02", rx2)];

        let finished = drain_parallel_events(&mut app);

        assert_eq!(finished.len(), 2);
        assert_eq!(app.parallel[0].status, AgentSlotStatus::Concluido);
        assert_eq!(app.parallel[1].status, AgentSlotStatus::Erro);
    }

    #[test]
    fn disconnected_parallel_channel_marks_slot_as_error() {
        let mut app = App::new(PathBuf::from("."), PathBuf::from("sdd"));
        let (tx, rx) = mpsc::channel();
        drop(tx);
        app.parallel = vec![handle("T-01", rx)];

        let finished = drain_parallel_events(&mut app);

        assert!(finished.is_empty());
        assert_eq!(app.parallel[0].status, AgentSlotStatus::Erro);
    }
}
