//! Command engine: typed command enum, registry, and execution bridge.
//!
//! Maps every REPL slash command and lifecycle operation into a single `Command`
//! enum with typed parameters. `CommandRegistry` parses raw input and delegates
//! execution to the existing `cmd_*` handlers in `repl.rs`.

use std::collections::HashMap;

use quicproquo_proto::node_capnp::node_service;

use super::repl::{Input, SlashCommand, parse_input};
use super::session::SessionState;

// ── Comparison operator for assert conditions ────────────────────────────────

/// Comparison operator used in playbook assertions.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "playbook", derive(serde::Serialize, serde::Deserialize))]
pub enum CmpOp {
    Eq,
    Ne,
    Gt,
    Lt,
    Gte,
    Lte,
}

impl CmpOp {
    /// Evaluate this comparison: `lhs <op> rhs`.
    pub fn eval(&self, lhs: usize, rhs: usize) -> bool {
        match self {
            CmpOp::Eq => lhs == rhs,
            CmpOp::Ne => lhs != rhs,
            CmpOp::Gt => lhs > rhs,
            CmpOp::Lt => lhs < rhs,
            CmpOp::Gte => lhs >= rhs,
            CmpOp::Lte => lhs <= rhs,
        }
    }
}

// ── Assert conditions for playbook testing ───────────────────────────────────

/// Conditions that can be asserted in a playbook step.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "playbook", derive(serde::Serialize, serde::Deserialize))]
pub enum AssertCondition {
    Connected,
    LoggedIn,
    InConversation { name: String },
    MessageCount { op: CmpOp, count: usize },
    MemberCount { op: CmpOp, count: usize },
    Custom { expression: String },
}

// ── Command enum ─────────────────────────────────────────────────────────────

/// Every operation the client can perform, with typed parameters.
///
/// This is a superset of `SlashCommand` — it adds lifecycle operations
/// (`Connect`, `Login`, `Register`, `SendMessage`, `Wait`, `Assert`, `SetVar`)
/// that are needed for non-interactive / playbook execution.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "playbook", derive(serde::Serialize, serde::Deserialize))]
pub enum Command {
    // ── Lifecycle (not in SlashCommand) ──────────────────────────────────
    Connect {
        server: String,
        ca_cert: Option<String>,
        insecure: bool,
    },
    Login {
        username: String,
        password: String,
    },
    Register {
        username: String,
        password: String,
    },
    SendMessage {
        text: String,
    },
    Wait {
        duration_ms: u64,
    },
    Assert {
        condition: AssertCondition,
    },
    SetVar {
        name: String,
        value: String,
    },

    // ── SlashCommand mirror ─────────────────────────────────────────────
    Help,
    Quit,
    Whoami,
    List,
    Switch { target: String },
    Dm { username: String },
    CreateGroup { name: String },
    Invite { target: String },
    Remove { target: String },
    Leave,
    Join,
    Members,
    GroupInfo,
    Rename { name: String },
    History { count: usize },

    // Mesh
    MeshPeers,
    MeshServer { addr: String },
    MeshSend { peer_id: String, message: String },
    MeshBroadcast { topic: String, message: String },
    MeshSubscribe { topic: String },
    MeshRoute,
    MeshIdentity,
    MeshStore,

    // Security / crypto
    Verify { username: String },
    UpdateKey,
    Typing,
    TypingNotify { enabled: bool },
    React { emoji: String, index: Option<usize> },
    Edit { index: usize, new_text: String },
    Delete { index: usize },
    SendFile { path: String },
    Download { index: usize },
    DeleteAccount,
    Disappear { arg: Option<String> },
    Privacy { arg: Option<String> },
    VerifyFs,
    RotateAllKeys,
    Devices,
    RegisterDevice { name: String },
    RevokeDevice { id_prefix: String },
}

impl Command {
    /// Convert a `Command` to a `SlashCommand` when possible.
    ///
    /// Returns `None` for lifecycle commands that have no `SlashCommand`
    /// equivalent (`Connect`, `Login`, `Register`, `SendMessage`, `Wait`,
    /// `Assert`, `SetVar`).
    pub(crate) fn to_slash(&self) -> Option<SlashCommand> {
        match self.clone() {
            // Lifecycle — no SlashCommand equivalent
            Command::Connect { .. }
            | Command::Login { .. }
            | Command::Register { .. }
            | Command::SendMessage { .. }
            | Command::Wait { .. }
            | Command::Assert { .. }
            | Command::SetVar { .. } => None,

            // 1:1 mirror
            Command::Help => Some(SlashCommand::Help),
            Command::Quit => Some(SlashCommand::Quit),
            Command::Whoami => Some(SlashCommand::Whoami),
            Command::List => Some(SlashCommand::List),
            Command::Switch { target } => Some(SlashCommand::Switch { target }),
            Command::Dm { username } => Some(SlashCommand::Dm { username }),
            Command::CreateGroup { name } => Some(SlashCommand::CreateGroup { name }),
            Command::Invite { target } => Some(SlashCommand::Invite { target }),
            Command::Remove { target } => Some(SlashCommand::Remove { target }),
            Command::Leave => Some(SlashCommand::Leave),
            Command::Join => Some(SlashCommand::Join),
            Command::Members => Some(SlashCommand::Members),
            Command::GroupInfo => Some(SlashCommand::GroupInfo),
            Command::Rename { name } => Some(SlashCommand::Rename { name }),
            Command::History { count } => Some(SlashCommand::History { count }),
            Command::MeshPeers => Some(SlashCommand::MeshPeers),
            Command::MeshServer { addr } => Some(SlashCommand::MeshServer { addr }),
            Command::MeshSend { peer_id, message } => {
                Some(SlashCommand::MeshSend { peer_id, message })
            }
            Command::MeshBroadcast { topic, message } => {
                Some(SlashCommand::MeshBroadcast { topic, message })
            }
            Command::MeshSubscribe { topic } => Some(SlashCommand::MeshSubscribe { topic }),
            Command::MeshRoute => Some(SlashCommand::MeshRoute),
            Command::MeshIdentity => Some(SlashCommand::MeshIdentity),
            Command::MeshStore => Some(SlashCommand::MeshStore),
            Command::Verify { username } => Some(SlashCommand::Verify { username }),
            Command::UpdateKey => Some(SlashCommand::UpdateKey),
            Command::Typing => Some(SlashCommand::Typing),
            Command::TypingNotify { enabled } => Some(SlashCommand::TypingNotify { enabled }),
            Command::React { emoji, index } => Some(SlashCommand::React { emoji, index }),
            Command::Edit { index, new_text } => Some(SlashCommand::Edit { index, new_text }),
            Command::Delete { index } => Some(SlashCommand::Delete { index }),
            Command::SendFile { path } => Some(SlashCommand::SendFile { path }),
            Command::Download { index } => Some(SlashCommand::Download { index }),
            Command::DeleteAccount => Some(SlashCommand::DeleteAccount),
            Command::Disappear { arg } => Some(SlashCommand::Disappear { arg }),
            Command::Privacy { arg } => Some(SlashCommand::Privacy { arg }),
            Command::VerifyFs => Some(SlashCommand::VerifyFs),
            Command::RotateAllKeys => Some(SlashCommand::RotateAllKeys),
            Command::Devices => Some(SlashCommand::Devices),
            Command::RegisterDevice { name } => Some(SlashCommand::RegisterDevice { name }),
            Command::RevokeDevice { id_prefix } => {
                Some(SlashCommand::RevokeDevice { id_prefix })
            }
        }
    }
}

// ── CommandResult ────────────────────────────────────────────────────────────

/// Outcome of executing a single `Command`.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "playbook", derive(serde::Serialize, serde::Deserialize))]
pub struct CommandResult {
    pub success: bool,
    pub output: Option<String>,
    pub error: Option<String>,
    /// Structured key-value outputs for variable capture in playbooks.
    pub data: HashMap<String, String>,
}

impl CommandResult {
    fn ok() -> Self {
        Self {
            success: true,
            output: None,
            error: None,
            data: HashMap::new(),
        }
    }

    fn err(msg: String) -> Self {
        Self {
            success: false,
            output: None,
            error: Some(msg),
            data: HashMap::new(),
        }
    }
}

// ── CommandRegistry ──────────────────────────────────────────────────────────

/// Parses raw input into `Command` and delegates execution to the existing
/// REPL handlers.
pub struct CommandRegistry;

impl CommandRegistry {
    /// Parse a raw input line into a `Command`.
    ///
    /// Returns `None` for empty input. Returns `Some(Command::SendMessage)`
    /// for plain chat text. Slash commands are parsed via the existing
    /// `parse_input` function.
    pub fn parse(line: &str) -> Option<Command> {
        match parse_input(line) {
            Input::Empty => None,
            Input::ChatMessage(text) => Some(Command::SendMessage { text }),
            Input::Slash(sc) => Some(slash_to_command(sc)),
        }
    }

    /// Execute a `Command`, delegating slash commands to the existing
    /// `handle_slash` dispatch and handling lifecycle commands directly.
    ///
    /// Currently, output from `cmd_*` handlers goes to stdout (unchanged).
    /// `CommandResult` captures success/failure status; stdout capture can
    /// be added later.
    pub async fn execute(
        cmd: &Command,
        session: &mut SessionState,
        client: &node_service::Client,
    ) -> CommandResult {
        match cmd {
            Command::Wait { duration_ms } => {
                tokio::time::sleep(std::time::Duration::from_millis(*duration_ms)).await;
                CommandResult::ok()
            }
            Command::SetVar { name, value } => {
                let mut result = CommandResult::ok();
                result.data.insert(name.clone(), value.clone());
                result
            }
            Command::Assert { condition } => execute_assert(condition, session),
            Command::Connect { .. } | Command::Login { .. } | Command::Register { .. } => {
                // These lifecycle commands require external context (endpoint,
                // OPAQUE state) that lives outside SessionState. The playbook
                // executor will handle them directly; calling execute() for
                // them is an error.
                CommandResult::err(
                    "lifecycle commands (connect/login/register) must be handled by the playbook executor".into(),
                )
            }
            Command::SendMessage { text } => {
                match super::repl::do_send(session, client, text).await {
                    Ok(()) => CommandResult::ok(),
                    Err(e) => CommandResult::err(format!("{e:#}")),
                }
            }
            Command::Quit => CommandResult::ok(),
            other => {
                // All remaining variants have a SlashCommand equivalent.
                if let Some(sc) = other.to_slash() {
                    match execute_slash(session, client, sc).await {
                        Ok(()) => CommandResult::ok(),
                        Err(e) => CommandResult::err(format!("{e:#}")),
                    }
                } else {
                    CommandResult::err("command has no slash equivalent".into())
                }
            }
        }
    }
}

// ── Conversion helpers ──────────────────────────────────────────────────────

/// Convert a `SlashCommand` into the corresponding `Command`.
fn slash_to_command(sc: SlashCommand) -> Command {
    match sc {
        SlashCommand::Help => Command::Help,
        SlashCommand::Quit => Command::Quit,
        SlashCommand::Whoami => Command::Whoami,
        SlashCommand::List => Command::List,
        SlashCommand::Switch { target } => Command::Switch { target },
        SlashCommand::Dm { username } => Command::Dm { username },
        SlashCommand::CreateGroup { name } => Command::CreateGroup { name },
        SlashCommand::Invite { target } => Command::Invite { target },
        SlashCommand::Remove { target } => Command::Remove { target },
        SlashCommand::Leave => Command::Leave,
        SlashCommand::Join => Command::Join,
        SlashCommand::Members => Command::Members,
        SlashCommand::GroupInfo => Command::GroupInfo,
        SlashCommand::Rename { name } => Command::Rename { name },
        SlashCommand::History { count } => Command::History { count },
        SlashCommand::MeshPeers => Command::MeshPeers,
        SlashCommand::MeshServer { addr } => Command::MeshServer { addr },
        SlashCommand::MeshSend { peer_id, message } => Command::MeshSend { peer_id, message },
        SlashCommand::MeshBroadcast { topic, message } => {
            Command::MeshBroadcast { topic, message }
        }
        SlashCommand::MeshSubscribe { topic } => Command::MeshSubscribe { topic },
        SlashCommand::MeshRoute => Command::MeshRoute,
        SlashCommand::MeshIdentity => Command::MeshIdentity,
        SlashCommand::MeshStore => Command::MeshStore,
        SlashCommand::Verify { username } => Command::Verify { username },
        SlashCommand::UpdateKey => Command::UpdateKey,
        SlashCommand::Typing => Command::Typing,
        SlashCommand::TypingNotify { enabled } => Command::TypingNotify { enabled },
        SlashCommand::React { emoji, index } => Command::React { emoji, index },
        SlashCommand::Edit { index, new_text } => Command::Edit { index, new_text },
        SlashCommand::Delete { index } => Command::Delete { index },
        SlashCommand::SendFile { path } => Command::SendFile { path },
        SlashCommand::Download { index } => Command::Download { index },
        SlashCommand::DeleteAccount => Command::DeleteAccount,
        SlashCommand::Disappear { arg } => Command::Disappear { arg },
        SlashCommand::Privacy { arg } => Command::Privacy { arg },
        SlashCommand::VerifyFs => Command::VerifyFs,
        SlashCommand::RotateAllKeys => Command::RotateAllKeys,
        SlashCommand::Devices => Command::Devices,
        SlashCommand::RegisterDevice { name } => Command::RegisterDevice { name },
        SlashCommand::RevokeDevice { id_prefix } => Command::RevokeDevice { id_prefix },
    }
}

// ── Execution helpers ───────────────────────────────────────────────────────

/// Execute a `SlashCommand` using the existing `cmd_*` handlers from `repl.rs`.
///
/// This duplicates the dispatch table from `handle_slash` but returns
/// `anyhow::Result<()>` instead of printing errors inline — the caller
/// decides how to surface errors.
async fn execute_slash(
    session: &mut SessionState,
    client: &node_service::Client,
    cmd: SlashCommand,
) -> anyhow::Result<()> {
    use super::repl::*;
    match cmd {
        SlashCommand::Help => {
            print_help();
            Ok(())
        }
        SlashCommand::Quit => Ok(()),
        SlashCommand::Whoami => cmd_whoami(session),
        SlashCommand::List => cmd_list(session),
        SlashCommand::Switch { target } => cmd_switch(session, &target),
        SlashCommand::Dm { username } => cmd_dm(session, client, &username).await,
        SlashCommand::CreateGroup { name } => cmd_create_group(session, &name),
        SlashCommand::Invite { target } => cmd_invite(session, client, &target).await,
        SlashCommand::Remove { target } => cmd_remove(session, client, &target).await,
        SlashCommand::Leave => cmd_leave(session, client).await,
        SlashCommand::Join => cmd_join(session, client).await,
        SlashCommand::Members => cmd_members(session, client).await,
        SlashCommand::GroupInfo => cmd_group_info(session, client).await,
        SlashCommand::Rename { name } => cmd_rename(session, &name),
        SlashCommand::History { count } => cmd_history(session, count),
        SlashCommand::MeshPeers => cmd_mesh_peers(),
        SlashCommand::MeshServer { addr } => {
            super::display::print_status(&format!(
                "mesh server hint: reconnect with --server {addr} to use this node"
            ));
            Ok(())
        }
        SlashCommand::MeshSend { peer_id, message } => cmd_mesh_send(&peer_id, &message),
        SlashCommand::MeshBroadcast { topic, message } => cmd_mesh_broadcast(&topic, &message),
        SlashCommand::MeshSubscribe { topic } => cmd_mesh_subscribe(&topic),
        SlashCommand::MeshRoute => cmd_mesh_route(session),
        SlashCommand::MeshIdentity => cmd_mesh_identity(session),
        SlashCommand::MeshStore => cmd_mesh_store(session),
        SlashCommand::Verify { username } => cmd_verify(session, client, &username).await,
        SlashCommand::UpdateKey => cmd_update_key(session, client).await,
        SlashCommand::Typing => cmd_typing(session, client).await,
        SlashCommand::TypingNotify { enabled } => {
            session.typing_notify_enabled = enabled;
            super::display::print_status(&format!(
                "typing notifications {}",
                if enabled { "enabled" } else { "disabled" }
            ));
            Ok(())
        }
        SlashCommand::React { emoji, index } => cmd_react(session, client, &emoji, index).await,
        SlashCommand::Edit { index, new_text } => {
            cmd_edit(session, client, index, &new_text).await
        }
        SlashCommand::Delete { index } => cmd_delete(session, client, index).await,
        SlashCommand::SendFile { path } => cmd_send_file(session, client, &path).await,
        SlashCommand::Download { index } => cmd_download(session, client, index).await,
        SlashCommand::DeleteAccount => cmd_delete_account(session, client).await,
        SlashCommand::Disappear { arg } => cmd_disappear(session, arg.as_deref()),
        SlashCommand::Privacy { arg } => cmd_privacy(session, arg.as_deref()),
        SlashCommand::VerifyFs => cmd_verify_fs(session),
        SlashCommand::RotateAllKeys => cmd_rotate_all_keys(session, client).await,
        SlashCommand::Devices => cmd_devices(client).await,
        SlashCommand::RegisterDevice { name } => cmd_register_device(client, &name).await,
        SlashCommand::RevokeDevice { id_prefix } => cmd_revoke_device(client, &id_prefix).await,
    }
}

/// Assert a condition against the current session state.
fn execute_assert(condition: &AssertCondition, session: &SessionState) -> CommandResult {
    match condition {
        AssertCondition::Connected => {
            // We have a session => we got past connect. Always true when
            // execute() is called with a valid client reference.
            CommandResult::ok()
        }
        AssertCondition::LoggedIn => {
            let guard = crate::AUTH_CONTEXT
                .read()
                .expect("AUTH_CONTEXT poisoned");
            if guard.is_some() {
                CommandResult::ok()
            } else {
                CommandResult::err("not logged in".into())
            }
        }
        AssertCondition::InConversation { name } => {
            if let Some(display) = session.active_display_name() {
                if display.contains(name.as_str()) {
                    CommandResult::ok()
                } else {
                    CommandResult::err(format!(
                        "active conversation is '{display}', expected '{name}'"
                    ))
                }
            } else {
                CommandResult::err("no active conversation".into())
            }
        }
        AssertCondition::MessageCount { op, count } => {
            let actual = session
                .active_conversation
                .as_ref()
                .and_then(|id| session.conv_store.load_all_messages(id).ok())
                .map(|msgs| msgs.len())
                .unwrap_or(0);
            if op.eval(actual, *count) {
                CommandResult::ok()
            } else {
                CommandResult::err(format!(
                    "message count assertion failed: {actual} {op:?} {count}"
                ))
            }
        }
        AssertCondition::MemberCount { op, count } => {
            let actual = session
                .active_conversation
                .as_ref()
                .and_then(|id| session.members.get(id))
                .map(|m| m.member_identities().len())
                .unwrap_or(0);
            if op.eval(actual, *count) {
                CommandResult::ok()
            } else {
                CommandResult::err(format!(
                    "member count assertion failed: {actual} {op:?} {count}"
                ))
            }
        }
        AssertCondition::Custom { expression } => {
            // Custom expressions are not evaluated yet; always pass.
            let mut result = CommandResult::ok();
            result.data.insert("expression".into(), expression.clone());
            result
        }
    }
}

