//! Matrix on_room_message handler — receives an incoming Matrix room event and
//! dispatches it to handle_message after access checks and command routing.

use crate::chat::util::is_permission_approval;
use crate::http::context::PermissionDecision;
use crate::slog;
use matrix_sdk::{
    Client,
    event_handler::Ctx,
    room::Room,
    ruma::events::room::message::{MessageType, OriginalSyncRoomMessageEvent},
};
use std::sync::Arc;

use super::super::context::BotContext;
use super::super::format::markdown_to_html;
use super::super::mentions::{is_addressed_to_other, is_reply_to_bot, mentions_bot};
use super::super::verification::check_sender_verified;

use super::handle_message;

pub(in crate::chat::transport::matrix::bot) async fn on_room_message(
    ev: OriginalSyncRoomMessageEvent,
    room: Room,
    client: Client,
    Ctx(ctx): Ctx<BotContext>,
) {
    let incoming_room_id = room.room_id().to_owned();

    slog!(
        "[matrix-bot] Event received: room={} sender={}",
        incoming_room_id,
        ev.sender,
    );

    // Only handle messages from rooms we are configured to listen in.
    if !ctx.target_room_ids.iter().any(|r| r == &incoming_room_id) {
        slog!("[matrix-bot] Ignoring message from unconfigured room {incoming_room_id}");
        return;
    }

    // Ignore the bot's own messages to prevent echo loops.
    if ev.sender == ctx.matrix_user_id {
        return;
    }

    // Only respond to users on the allowlist (fail-closed).
    if !ctx.allowed_users.iter().any(|u| u == ev.sender.as_str()) {
        slog!(
            "[matrix-bot] Ignoring message from unauthorised user: {}",
            ev.sender
        );
        return;
    }

    // Only handle plain text messages.
    let (body, formatted_body) = match &ev.content.msgtype {
        MessageType::Text(t) => (t.body.clone(), t.formatted.as_ref().map(|f| f.body.clone())),
        _ => return,
    };

    // Only respond when the bot is directly addressed (mentioned by name/ID),
    // when the message is a reply to one of the bot's own messages, or when
    // ambient mode is enabled for this room.
    let is_addressed = mentions_bot(&body, formatted_body.as_deref(), &ctx.matrix_user_id)
        || is_reply_to_bot(ev.content.relates_to.as_ref(), &ctx.bot_sent_event_ids).await;
    let room_id_str = incoming_room_id.to_string();
    let is_ambient = ctx
        .services
        .ambient_rooms
        .lock()
        .unwrap()
        .contains(&room_id_str);

    // Always let "ambient on" through — it is the one command that must work
    // even when the bot is not mentioned and ambient mode is off, otherwise
    // there is no way to re-enable ambient mode without an @-mention.
    let is_ambient_on = body.to_ascii_lowercase().contains("ambient on");

    if !is_addressed && !is_ambient && !is_ambient_on {
        slog!(
            "[matrix-bot] Ignoring unaddressed message from {}",
            ev.sender
        );
        return;
    }

    // In ambient mode, ignore messages that are explicitly addressed to a
    // different entity (e.g. "sally: do X" or "@sally do X" when we are stu).
    // We still let through messages addressed to us and the "ambient on" command.
    if is_ambient
        && !is_addressed
        && !is_ambient_on
        && is_addressed_to_other(&body, &ctx.matrix_user_id, &ctx.services.bot_name)
    {
        slog!(
            "[matrix-bot] Ignoring ambient message addressed to another bot (sender={})",
            ev.sender
        );
        return;
    }

    // Reject commands from unencrypted rooms — E2EE is mandatory.
    if !room.encryption_state().is_encrypted() {
        slog!(
            "[matrix-bot] Rejecting message from {} — room {} is not encrypted. \
             Commands are only accepted from encrypted rooms.",
            ev.sender,
            incoming_room_id
        );
        return;
    }

    // Always verify that the sender has a cross-signing identity.
    // This check is unconditional and cannot be disabled via config.
    match check_sender_verified(&client, &ev.sender).await {
        Ok(true) => { /* sender has a cross-signing identity — proceed */ }
        Ok(false) => {
            slog!(
                "[matrix-bot] Rejecting message from {} — no cross-signing identity \
                 found in encrypted room {}",
                ev.sender,
                incoming_room_id
            );
            return;
        }
        Err(e) => {
            slog!(
                "[matrix-bot] Error checking verification for {}: {e} — \
                 rejecting message (fail-closed)",
                ev.sender
            );
            return;
        }
    }

    // If there is a pending permission prompt for this room, interpret the
    // message as a yes/no response instead of starting a new chat.
    {
        let mut pending = ctx.services.pending_perm_replies.lock().await;
        if let Some(tx) = pending.remove(incoming_room_id.as_str()) {
            let decision = if is_permission_approval(&body) {
                PermissionDecision::Approve
            } else {
                PermissionDecision::Deny
            };
            let _ = tx.send(decision);
            let confirmation = if decision == PermissionDecision::Approve {
                "Permission approved."
            } else {
                "Permission denied."
            };
            let html = markdown_to_html(confirmation);
            if let Ok(msg_id) = ctx
                .transport
                .send_message(&room_id_str, confirmation, &html)
                .await
                && let Ok(event_id) = msg_id.parse()
            {
                ctx.bot_sent_event_ids.lock().await.insert(event_id);
            }
            return;
        }
    }

    let sender = ev.sender.to_string();
    let user_message = body;
    slog!("[matrix-bot] Message from {sender}: {user_message}");

    // In gateway mode, resolve commands against the active project's root directory.
    // The gateway's own project_root is the gateway config dir; each project lives in
    // a subdirectory named after the project. Standalone mode is unaffected.
    let effective_root = ctx.effective_project_root().await;

    // ── Gateway command proxy ───────────────────────────────────────────
    // In gateway mode the bot has no local CRDT or project filesystem, so most
    // commands must be forwarded to the active project's `/api/bot/command`
    // endpoint.  Only a small set of gateway-local commands are handled here.
    if ctx.is_gateway() {
        // Commands that are meaningful on the gateway itself (no project state needed).
        const GATEWAY_LOCAL_COMMANDS: &[&str] =
            &["help", "ambient", "reset", "switch", "all_status"];

        let stripped = crate::chat::util::strip_bot_mention(
            &user_message,
            &ctx.services.bot_name,
            ctx.matrix_user_id.as_str(),
        )
        .trim()
        .trim_start_matches(|c: char| !c.is_alphanumeric())
        .to_string();

        let (cmd, args) = match stripped.split_once(char::is_whitespace) {
            Some((c, a)) => (c.to_ascii_lowercase(), a.trim().to_string()),
            None => (stripped.to_ascii_lowercase(), String::new()),
        };

        // Only proxy if the first word is a known bot command (sync or async).
        let is_known_command = !cmd.is_empty()
            && !GATEWAY_LOCAL_COMMANDS.contains(&cmd.as_str())
            && (crate::chat::commands::commands()
                .iter()
                .any(|c| c.name == cmd)
                || [
                    "assign", "start", "delete", "rebuild", "rmtree", "htop", "timer",
                ]
                .contains(&cmd.as_str()));

        if is_known_command {
            // Proxy to the active project server.
            let response = match ctx.proxy_bot_command(&cmd, &args).await {
                Some(r) => r,
                None => "No active project selected or project URL not configured.".to_string(),
            };
            let html = markdown_to_html(&response);
            if let Ok(msg_id) = ctx
                .transport
                .send_message(&room_id_str, &response, &html)
                .await
                && let Ok(event_id) = msg_id.parse()
            {
                ctx.bot_sent_event_ids.lock().await.insert(event_id);
            }
            return;
        }

        // `all_status` — aggregate pipeline status across all projects (gateway-only).
        if cmd == "all_status" {
            let project_urls = ctx.gateway_project_urls.clone();
            let client = reqwest::Client::new();
            let statuses =
                crate::gateway::fetch_all_project_pipeline_statuses(&project_urls, &client).await;
            let response = crate::gateway::format_aggregate_status_compact(&statuses);
            let html = markdown_to_html(&response);
            if let Ok(msg_id) = ctx
                .transport
                .send_message(&room_id_str, &response, &html)
                .await
                && let Ok(event_id) = msg_id.parse()
            {
                ctx.bot_sent_event_ids.lock().await.insert(event_id);
            }
            return;
        }

        // Gateway-local commands and freeform text fall through to normal handling below.
    }

    // Check for bot-level commands (help, status, ambient, …) before invoking
    // the LLM.  All commands are registered in commands.rs — no special-casing
    // needed here.
    let dispatch = super::super::super::commands::CommandDispatch {
        services: &ctx.services,
        project_root: &effective_root,
        bot_user_id: ctx.matrix_user_id.as_str(),
        room_id: &room_id_str,
    };
    if let Some((response, response_html)) =
        super::super::super::commands::try_handle_command_with_html(&dispatch, &user_message)
    {
        slog!("[matrix-bot] Handled bot command from {sender}");
        if let Ok(msg_id) = ctx
            .transport
            .send_message(&room_id_str, &response, &response_html)
            .await
            && let Ok(event_id) = msg_id.parse()
        {
            ctx.bot_sent_event_ids.lock().await.insert(event_id);
        }
        return;
    }

    // Check for the assign command, which requires async agent ops (stop +
    // start) and cannot be handled by the sync command registry.
    if let Some(assign_cmd) = super::super::super::assign::extract_assign_command(
        &user_message,
        &ctx.services.bot_name,
        ctx.matrix_user_id.as_str(),
    ) {
        let response = match assign_cmd {
            super::super::super::assign::AssignCommand::Assign {
                story_number,
                model,
            } => {
                slog!(
                    "[matrix-bot] Handling assign command from {sender}: story {story_number} model={model}"
                );
                super::super::super::assign::handle_assign(
                    &ctx.services.bot_name,
                    &story_number,
                    &model,
                    &effective_root,
                    &ctx.services.agents,
                )
                .await
            }
            super::super::super::assign::AssignCommand::BadArgs => {
                format!(
                    "Usage: `{} assign <number> <model>` (e.g. `assign 42 opus`)",
                    ctx.services.bot_name
                )
            }
        };
        let html = markdown_to_html(&response);
        if let Ok(msg_id) = ctx
            .transport
            .send_message(&room_id_str, &response, &html)
            .await
            && let Ok(event_id) = msg_id.parse()
        {
            ctx.bot_sent_event_ids.lock().await.insert(event_id);
        }
        return;
    }

    // Check for the htop command, which requires async Matrix access (Room)
    // and cannot be handled by the sync command registry.
    if let Some(htop_cmd) = super::super::super::htop::extract_htop_command(
        &user_message,
        &ctx.services.bot_name,
        ctx.matrix_user_id.as_str(),
    ) {
        slog!("[matrix-bot] Handling htop command from {sender}: {htop_cmd:?}");
        match htop_cmd {
            super::super::super::htop::HtopCommand::Stop => {
                super::super::super::htop::handle_htop_stop(
                    &*ctx.transport,
                    &room_id_str,
                    &ctx.htop_sessions,
                )
                .await;
            }
            super::super::super::htop::HtopCommand::Start { duration_secs } => {
                super::super::super::htop::handle_htop_start(
                    &ctx.transport,
                    &room_id_str,
                    &ctx.htop_sessions,
                    Arc::clone(&ctx.services.agents),
                    duration_secs,
                )
                .await;
            }
        }
        return;
    }

    // Check for the delete command, which requires async agent/worktree ops
    // and cannot be handled by the sync command registry.
    if let Some(del_cmd) = super::super::super::delete::extract_delete_command(
        &user_message,
        &ctx.services.bot_name,
        ctx.matrix_user_id.as_str(),
    ) {
        let response = match del_cmd {
            super::super::super::delete::DeleteCommand::Delete { story_number } => {
                slog!("[matrix-bot] Handling delete command from {sender}: story {story_number}");
                super::super::super::delete::handle_delete(
                    &ctx.services.bot_name,
                    &story_number,
                    &effective_root,
                    &ctx.services.agents,
                )
                .await
            }
            super::super::super::delete::DeleteCommand::BadArgs => {
                format!("Usage: `{} delete <number>`", ctx.services.bot_name)
            }
        };
        let html = markdown_to_html(&response);
        if let Ok(msg_id) = ctx
            .transport
            .send_message(&room_id_str, &response, &html)
            .await
            && let Ok(event_id) = msg_id.parse()
        {
            ctx.bot_sent_event_ids.lock().await.insert(event_id);
        }
        return;
    }

    // Check for the rmtree command, which requires async agent/worktree ops
    // and cannot be handled by the sync command registry.
    if let Some(rmtree_cmd) = super::super::super::rmtree::extract_rmtree_command(
        &user_message,
        &ctx.services.bot_name,
        ctx.matrix_user_id.as_str(),
    ) {
        let response = match rmtree_cmd {
            super::super::super::rmtree::RmtreeCommand::Rmtree { story_number } => {
                slog!("[matrix-bot] Handling rmtree command from {sender}: story {story_number}");
                super::super::super::rmtree::handle_rmtree(
                    &ctx.services.bot_name,
                    &story_number,
                    &effective_root,
                    &ctx.services.agents,
                )
                .await
            }
            super::super::super::rmtree::RmtreeCommand::BadArgs => {
                format!("Usage: `{} rmtree <number>`", ctx.services.bot_name)
            }
        };
        let html = markdown_to_html(&response);
        if let Ok(msg_id) = ctx
            .transport
            .send_message(&room_id_str, &response, &html)
            .await
            && let Ok(event_id) = msg_id.parse()
        {
            ctx.bot_sent_event_ids.lock().await.insert(event_id);
        }
        return;
    }

    // Check for the start command, which requires async agent ops and cannot
    // be handled by the sync command registry.
    if let Some(start_cmd) = super::super::super::start::extract_start_command(
        &user_message,
        &ctx.services.bot_name,
        ctx.matrix_user_id.as_str(),
    ) {
        let response = match start_cmd {
            super::super::super::start::StartCommand::Start {
                story_number,
                agent_hint,
            } => {
                slog!(
                    "[matrix-bot] Handling start command from {sender}: story {story_number} agent={agent_hint:?}"
                );
                super::super::super::start::handle_start(
                    &ctx.services.bot_name,
                    &story_number,
                    agent_hint.as_deref(),
                    &effective_root,
                    &ctx.services.agents,
                )
                .await
            }
            super::super::super::start::StartCommand::BadArgs => {
                format!(
                    "Usage: `{} start <number>` or `{} start <number> opus`",
                    ctx.services.bot_name, ctx.services.bot_name
                )
            }
        };
        let html = markdown_to_html(&response);
        if let Ok(msg_id) = ctx
            .transport
            .send_message(&room_id_str, &response, &html)
            .await
            && let Ok(event_id) = msg_id.parse()
        {
            ctx.bot_sent_event_ids.lock().await.insert(event_id);
        }
        return;
    }

    // Check for the reset command, which requires async access to the shared
    // conversation history and cannot be handled by the sync command registry.
    if super::super::super::reset::extract_reset_command(
        &user_message,
        &ctx.services.bot_name,
        ctx.matrix_user_id.as_str(),
    )
    .is_some()
    {
        slog!("[matrix-bot] Handling reset command from {sender}");
        let response = super::super::super::reset::handle_reset(
            &ctx.services.bot_name,
            &incoming_room_id,
            &ctx.history,
            &ctx.services.project_root,
        )
        .await;
        let html = markdown_to_html(&response);
        if let Ok(msg_id) = ctx
            .transport
            .send_message(&room_id_str, &response, &html)
            .await
            && let Ok(event_id) = msg_id.parse()
        {
            ctx.bot_sent_event_ids.lock().await.insert(event_id);
        }
        return;
    }

    // Check for the rebuild command, which requires async agent and process ops
    // and cannot be handled by the sync command registry.
    if super::super::super::rebuild::extract_rebuild_command(
        &user_message,
        &ctx.services.bot_name,
        ctx.matrix_user_id.as_str(),
    )
    .is_some()
    {
        slog!("[matrix-bot] Handling rebuild command from {sender}");
        // Acknowledge immediately — the rebuild may take a while or re-exec.
        let ack = "Rebuilding server… this may take a moment.";
        let ack_html = markdown_to_html(ack);
        if let Ok(msg_id) = ctx
            .transport
            .send_message(&room_id_str, ack, &ack_html)
            .await
            && let Ok(event_id) = msg_id.parse()
        {
            ctx.bot_sent_event_ids.lock().await.insert(event_id);
        }
        let response = super::super::super::rebuild::handle_rebuild(
            &ctx.services.bot_name,
            &ctx.services.project_root,
            &ctx.services.agents,
        )
        .await;
        let html = markdown_to_html(&response);
        if let Ok(msg_id) = ctx
            .transport
            .send_message(&room_id_str, &response, &html)
            .await
            && let Ok(event_id) = msg_id.parse()
        {
            ctx.bot_sent_event_ids.lock().await.insert(event_id);
        }
        return;
    }

    // In gateway mode, handle the "switch <project>" command to change the
    // active project without invoking the LLM.
    if let Some(ref active_project) = ctx.gateway_active_project {
        let stripped = crate::chat::util::strip_bot_mention(
            &user_message,
            &ctx.services.bot_name,
            ctx.matrix_user_id.as_str(),
        )
        .trim()
        .trim_start_matches(|c: char| !c.is_alphanumeric())
        .to_string();

        let (cmd, arg) = match stripped.split_once(char::is_whitespace) {
            Some((c, a)) => (c.to_string(), a.trim().to_string()),
            None => (stripped.clone(), String::new()),
        };

        if cmd.eq_ignore_ascii_case("switch") {
            let response = if arg.is_empty() {
                let available = ctx.gateway_projects.join(", ");
                format!("Usage: `switch <project>`. Available projects: {available}")
            } else if ctx.gateway_projects.iter().any(|p| p == &arg) {
                *active_project.write().await = arg.clone();
                format!("Switched to project **{arg}**.")
            } else {
                let available = ctx.gateway_projects.join(", ");
                format!("Unknown project `{arg}`. Available: {available}")
            };
            let html = markdown_to_html(&response);
            if let Ok(msg_id) = ctx
                .transport
                .send_message(&room_id_str, &response, &html)
                .await
                && let Ok(event_id) = msg_id.parse()
            {
                ctx.bot_sent_event_ids.lock().await.insert(event_id);
            }
            return;
        }
    }

    // Check for the timer command, which requires async file I/O and cannot
    // be handled by the sync command registry.
    if let Some(timer_cmd) = crate::service::timer::extract_timer_command(
        &user_message,
        &ctx.services.bot_name,
        ctx.matrix_user_id.as_str(),
    ) {
        slog!("[matrix-bot] Handling timer command from {sender}: {timer_cmd:?}");
        let response = crate::service::timer::handle_timer_command(
            timer_cmd,
            &ctx.timer_store,
            &ctx.services.project_root,
        )
        .await;
        let html = markdown_to_html(&response);
        if let Ok(msg_id) = ctx
            .transport
            .send_message(&room_id_str, &response, &html)
            .await
            && let Ok(event_id) = msg_id.parse()
        {
            ctx.bot_sent_event_ids.lock().await.insert(event_id);
        }
        return;
    }

    // Spawn a separate task so the Matrix sync loop is not blocked while we
    // wait for the LLM response (which can take several seconds).
    tokio::spawn(async move {
        handle_message(room_id_str, incoming_room_id, ctx, sender, user_message).await;
    });
}
