pub use crate::protocol::{WsInbound, WsOutbound, serialize_inbound, serialize_outbound};

mod oneshot;
mod tui;

pub use oneshot::{RunOptions, read_run_prompt, run_once};

use crate::client::tui::app::{App, MessageRole};
use crate::client::tui::event::{
    handle_key_event, handle_paste, request_history, request_session_list, send,
};
use crate::client::tui::ui::render_ui;
use crossterm::{
    event::{self, DisableBracketedPaste, EnableBracketedPaste, Event, KeyEventKind},
    execute,
    terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use futures_util::StreamExt;
use ratatui::{Terminal, prelude::CrosstermBackend};
use std::io;
use std::{fs, path::PathBuf};
use tokio_tungstenite::{
    connect_async,
    tungstenite::{Message, client::IntoClientRequest, http::header},
};

pub async fn run(
    gateway_url: &str,
    pair_code: Option<&str>,
) -> Result<(), Box<dyn std::error::Error>> {
    let client_id = load_or_create_client_id();
    let separator = if gateway_url.contains('?') { '&' } else { '?' };
    let connect_url = format!("{gateway_url}{separator}client_id={client_id}");
    let token = if let Some(code) = pair_code {
        let token = exchange_pairing_code(gateway_url, code).await?;
        save_auth_token(&token)?;
        Some(token)
    } else {
        load_auth_token()
    };
    let mut request = connect_url.into_client_request()?;
    if let Some(token) = &token {
        request
            .headers_mut()
            .insert(header::AUTHORIZATION, format!("Bearer {token}").parse()?);
    }
    let (ws_stream, _) = connect_async(request).await.map_err(|error| {
        format!(
            "gateway connection failed: {error}. If pairing is required, run `picobot pair` then `picobot chat --pair-code <CODE>`"
        )
    })?;
    tracing::info!("Connected to gateway");

    let (ws_sender, ws_receiver) = ws_stream.split();

    let mut app = App::new();
    app.http_base_url = gateway_http_base_url(gateway_url)?;
    app.auth_token = token;
    app.client_id = client_id;
    app.ws_sender = Some(ws_sender);
    app.ws_receiver = Some(ws_receiver);

    enable_raw_mode()?;
    let mut stdout = io::stdout();
    execute!(stdout, EnterAlternateScreen, EnableBracketedPaste)?;
    let backend = CrosstermBackend::new(stdout);
    let mut terminal = Terminal::new(backend)?;
    terminal.clear()?;

    let result = run_app(&mut terminal, app).await;

    // Cleanup terminal, ignore errors
    let _ = execute!(
        terminal.backend_mut(),
        DisableBracketedPaste,
        LeaveAlternateScreen
    );
    let _ = disable_raw_mode();
    let _ = terminal.show_cursor();

    result
}

fn gateway_http_base_url(gateway_url: &str) -> Result<String, Box<dyn std::error::Error>> {
    let mut url = reqwest::Url::parse(gateway_url)?;
    let scheme = match url.scheme() {
        "ws" => "http",
        "wss" => "https",
        "http" => "http",
        "https" => "https",
        other => return Err(format!("unsupported gateway URL scheme: {other}").into()),
    };
    url.set_scheme(scheme)
        .map_err(|_| "failed to set gateway URL scheme")?;
    url.set_path("");
    url.set_query(None);
    url.set_fragment(None);
    Ok(url.to_string().trim_end_matches('/').to_string())
}

pub async fn reload_gateway(gateway_url: &str) -> Result<String, Box<dyn std::error::Error>> {
    let base = gateway_http_base_url(gateway_url)?;
    let mut request = reqwest::Client::new().post(format!("{base}/api/config/reload"));
    if let Some(token) = load_auth_token() {
        request = request.bearer_auth(token);
    }
    let response = request.send().await?;
    let status = response.status();
    let body: serde_json::Value = response.json().await?;
    if !status.is_success() {
        return Err(body
            .get("error")
            .and_then(serde_json::Value::as_str)
            .unwrap_or("configuration reload failed")
            .to_string()
            .into());
    }
    Ok(body
        .get("message")
        .and_then(serde_json::Value::as_str)
        .unwrap_or("configuration reload scheduled")
        .to_string())
}

async fn exchange_pairing_code(
    gateway_url: &str,
    code: &str,
) -> Result<String, Box<dyn std::error::Error>> {
    let mut url = reqwest::Url::parse(gateway_url)?;
    let scheme = match url.scheme() {
        "ws" => "http",
        "wss" => "https",
        "http" => "http",
        "https" => "https",
        other => return Err(format!("unsupported gateway URL scheme: {other}").into()),
    };
    url.set_scheme(scheme)
        .map_err(|_| "failed to set gateway URL scheme")?;
    url.set_path("/api/auth/pair");
    url.set_query(None);
    url.set_fragment(None);
    let response = reqwest::Client::new()
        .post(url)
        .json(&serde_json::json!({ "code": code }))
        .send()
        .await?;
    let status = response.status();
    let body: serde_json::Value = response.json().await?;
    if !status.is_success() {
        return Err(body
            .get("error")
            .and_then(serde_json::Value::as_str)
            .unwrap_or("pairing failed")
            .to_string()
            .into());
    }
    body.get("token")
        .and_then(serde_json::Value::as_str)
        .map(str::to_string)
        .ok_or_else(|| "gateway did not return an auth token".into())
}

fn auth_token_path() -> Option<PathBuf> {
    dirs::home_dir().map(|home| home.join(".picobot").join("tui_auth_token"))
}

fn load_auth_token() -> Option<String> {
    fs::read_to_string(auth_token_path()?)
        .ok()
        .map(|token| token.trim().to_string())
        .filter(|token| !token.is_empty())
}

fn save_auth_token(token: &str) -> Result<(), Box<dyn std::error::Error>> {
    let path = auth_token_path().ok_or("home directory is unavailable")?;
    let parent = path.parent().ok_or("invalid auth token path")?;
    fs::create_dir_all(parent)?;
    #[cfg(unix)]
    {
        use std::io::Write;
        use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
        let mut file = fs::OpenOptions::new()
            .create(true)
            .truncate(true)
            .write(true)
            .mode(0o600)
            .open(&path)?;
        file.write_all(token.as_bytes())?;
        file.sync_all()?;
        fs::set_permissions(path, fs::Permissions::from_mode(0o600))?;
    }
    #[cfg(not(unix))]
    fs::write(&path, token)?;
    Ok(())
}

fn load_or_create_client_id() -> String {
    let generated = uuid::Uuid::new_v4().simple().to_string();
    let Some(home) = dirs::home_dir() else {
        return generated;
    };
    let dir = home.join(".picobot");
    let path: PathBuf = dir.join("tui_client_id");
    if let Ok(value) = fs::read_to_string(&path) {
        let value = value.trim();
        if !value.is_empty()
            && value.len() <= 64
            && value
                .bytes()
                .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-' || byte == b'_')
        {
            return value.to_string();
        }
    }
    if fs::create_dir_all(dir).is_ok() {
        let _ = fs::write(path, &generated);
    }
    generated
}

async fn run_app(
    terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
    mut app: App,
) -> Result<(), Box<dyn std::error::Error>> {
    let mut ws_receiver = app.ws_receiver.take().unwrap();
    let mut event_reader = event::EventStream::new();
    let mut ws_open = true;

    send(&mut app, WsInbound::GetSlashCommands).await;
    request_session_list(&mut app).await;

    loop {
        terminal.draw(|f| render_ui(f, &app))?;

        tokio::select! {
            msg = ws_receiver.next(), if ws_open => {
                match msg {
                    Some(Ok(Message::Text(text))) => {
                        if let Ok(outbound) = serde_json::from_str::<WsOutbound>(&text) {
                            handle_ws_message(&mut app, outbound).await;
                        }
                    }
                    Some(Ok(Message::Close(_))) | None => {
                        tracing::info!("Gateway disconnected");
                        app.connected = false;
                        app.ws_sender = None;
                        ws_open = false;
                        app.status_message = Some("Gateway 连接已关闭；按两次 Ctrl+C 退出".to_string());
                    }
                    Some(Err(error)) => {
                        app.connected = false;
                        app.ws_sender = None;
                        ws_open = false;
                        app.status_message = Some(format!("Gateway 连接错误：{error}"));
                    }
                    _ => {}
                }
            }
            event_result = event_reader.next() => {
                match event_result {
                    Some(Ok(Event::Key(key))) if key.kind != KeyEventKind::Release => {
                        handle_key_event(&mut app, key).await;
                    }
                    Some(Ok(Event::Paste(text))) => handle_paste(&mut app, &text),
                    Some(Err(error)) => {
                        app.status_message = Some(format!("终端输入错误：{error}"));
                    }
                    _ => {}
                }
            }
        }

        if app.should_quit {
            break;
        }
    }

    Ok(())
}

async fn handle_ws_message(app: &mut App, outbound: WsOutbound) {
    match outbound {
        WsOutbound::TurnUpdated { snapshot } => {
            let terminal = snapshot.status != crate::session::TurnStatus::Running;
            let completed = snapshot.status == crate::session::TurnStatus::Completed;
            let session_id = snapshot.session_id.clone();
            if terminal {
                app.pending_responses = app.pending_responses.saturating_sub(1);
            }
            if app.apply_turn_snapshot(snapshot) {
                if terminal {
                    app.status_message = None;
                    if app.current_session_id.as_deref() == Some(&session_id) {
                        if !completed {
                            request_history(app, session_id).await;
                        }
                    } else {
                        app.status_message = Some("另一个会话已完成响应".to_string());
                    }
                    request_session_list(app).await;
                } else {
                    app.status_message = Some("正在生成回复…".to_string());
                }
            } else if terminal && app.current_session_id.as_deref() != Some(&session_id) {
                app.status_message = Some("另一个会话已完成响应".to_string());
            }
        }
        WsOutbound::TurnCommitted {
            session_id,
            history_revision,
            messages,
        } => {
            app.apply_turn_commit(&session_id, history_revision, messages);
        }
        WsOutbound::AssistantResponse {
            id,
            content,
            attachments,
            session_id,
            ..
        } => {
            app.pending_responses = app.pending_responses.saturating_sub(1);
            app.status_message = None;
            if session_id
                .as_ref()
                .is_none_or(|session_id| app.current_session_id.as_ref() == Some(session_id))
            {
                app.add_message_with_attachments(id, MessageRole::Assistant, content, attachments);
                if let Some(current) = app.current_session_id.clone() {
                    request_history(app, current).await;
                }
            } else {
                app.status_message = Some("另一个会话已完成响应".to_string());
            }
            request_session_list(app).await;
        }
        WsOutbound::Error { message, .. } => {
            app.pending_responses = app.pending_responses.saturating_sub(1);
            app.status_message = Some(message.clone());
            app.add_message(MessageRole::System, format!("Error: {}", message));
        }
        WsOutbound::SessionEstablished {
            session_id,
            capabilities,
        } => {
            app.connected = true;
            app.file_transfer_supported =
                capabilities.iter().any(|value| value == "file_transfer_v1");
            app.set_current_session(Some(session_id.clone()));
            request_history(app, session_id).await;
            request_session_list(app).await;
        }
        WsOutbound::SessionCreated { session_id, .. } => {
            app.set_current_session(Some(session_id.clone()));
            app.status_message = None;
            request_history(app, session_id).await;
            request_session_list(app).await;
        }
        WsOutbound::SessionList {
            sessions,
            current_session_id,
        } => {
            app.set_sessions(sessions);
            if let Some(id) = current_session_id {
                let changed = app.current_session_id.as_deref() != Some(&id);
                app.set_current_session(Some(id.clone()));
                if changed {
                    request_history(app, id).await;
                }
            }
        }
        WsOutbound::SessionLoaded { session_id, .. } => {
            app.set_current_session(Some(session_id.clone()));
            request_history(app, session_id).await;
            request_session_list(app).await;
        }
        WsOutbound::SessionHistory {
            session_id,
            messages,
        } => app.set_history(&session_id, messages),
        // The first Todo UI is WebUI-only. CLI keeps receiving ordinary task
        // notifications and may inspect plans through /todo.
        WsOutbound::SessionPlan { .. }
        | WsOutbound::SessionStats { .. }
        | WsOutbound::PlanUpdated { .. }
        | WsOutbound::SessionAgentRuns { .. }
        | WsOutbound::AgentRunUpdated { .. }
        | WsOutbound::AgentEventUpdated { .. } => {}
        WsOutbound::SessionRenamed { session_id, title } => {
            if let Some(session) = app
                .sessions
                .iter_mut()
                .find(|session| session.session_id == session_id)
            {
                session.title = title;
            }
            request_session_list(app).await;
        }
        WsOutbound::SessionArchived { session_id } => {
            app.sessions
                .retain(|session| session.session_id != session_id);
            request_session_list(app).await;
        }
        WsOutbound::SessionDeleted { session_id } => {
            if app.current_session_id.as_ref() == Some(&session_id) {
                app.set_current_session(None);
            }
            app.sessions
                .retain(|session| session.session_id != session_id);
            request_session_list(app).await;
        }
        WsOutbound::HistoryCleared { session_id } => {
            if app.current_session_id.as_deref() == Some(&session_id) {
                app.messages.clear();
            }
            app.status_message = None;
            request_session_list(app).await;
        }
        WsOutbound::SlashCommandsList { commands } => {
            app.set_commands(commands);
        }
        WsOutbound::Pong => {}
        WsOutbound::CommandExecuted { message } => {
            app.pending_responses = app.pending_responses.saturating_sub(1);
            app.status_message = None;
            app.add_message(MessageRole::System, message);
            request_session_list(app).await;
        }
        WsOutbound::SystemNotification {
            content,
            session_id,
        } => {
            if session_id
                .as_ref()
                .is_none_or(|session_id| app.current_session_id.as_ref() == Some(session_id))
            {
                app.add_message(MessageRole::System, content);
            }
        }
    }
}
