//! Background task that registers as a permission responder for the bot's
//! lifetime and forwards permission requests to the configured Matrix room.
//!
//! Before story 884, each chat message handler acquired `perm_rx` for the
//! duration of one chat_fut and dropped it afterwards. That meant whenever
//! the bot wasn't actively responding, `prompt_permission` auto-denied any
//! spawned coder bash call as "no interactive session" — making unattended
//! coder work impossible. This task stays registered continuously while the
//! bot is connected, so requests can flow at any time.

use crate::chat::ChatTransport;
use crate::http::context::PermissionDecision;
use crate::services::Services;
use crate::slog;
use matrix_sdk::ruma::{OwnedEventId, OwnedRoomId};
use std::collections::HashSet;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Mutex as TokioMutex;

use super::format::markdown_to_html;

/// Spawn a background task that registers with `services.permission_registry`
/// for the bot's lifetime and forwards each incoming permission request to
/// `target_room` as a chat message. Replies (yes/no) are resolved by the
/// existing `on_room_message` handler via `pending_perm_replies`.
///
/// Returns the JoinHandle so the caller can keep ownership; the task exits
/// only when its private channel closes (bot shutdown).
pub fn spawn_permission_listener(
    services: Arc<Services>,
    transport: Arc<dyn ChatTransport>,
    target_room: OwnedRoomId,
    bot_sent_event_ids: Arc<TokioMutex<HashSet<OwnedEventId>>>,
) -> tokio::task::JoinHandle<()> {
    tokio::spawn(async move {
        let (_responder_guard, mut perm_rx) = services.permission_registry.register();
        let target_room_str = target_room.as_str().to_string();
        slog!("[matrix-bot] permission listener started; forwarding requests to {target_room_str}");

        while let Some(perm_fwd) = perm_rx.recv().await {
            let prompt_msg = format!(
                "**Permission Request**\n\n\
                 Tool: `{}`\n```json\n{}\n```\n\n\
                 Reply **yes** to approve or **no** to deny.",
                perm_fwd.tool_name,
                serde_json::to_string_pretty(&perm_fwd.tool_input)
                    .unwrap_or_else(|_| perm_fwd.tool_input.to_string()),
            );
            let html = markdown_to_html(&prompt_msg);
            if let Ok(msg_id) = transport
                .send_message(&target_room_str, &prompt_msg, &html)
                .await
                && let Ok(event_id) = msg_id.parse::<OwnedEventId>()
            {
                bot_sent_event_ids.lock().await.insert(event_id);
            }

            // Store the MCP oneshot sender so on_room_message can resolve it
            // when the user replies yes/no in the target room. Keyed by
            // request_id (not just room) so a second concurrent request for
            // the same room doesn't drop the first's sender.
            services
                .pending_perm_replies
                .insert(
                    target_room.to_string(),
                    perm_fwd.request_id.clone(),
                    perm_fwd.response_tx,
                )
                .await;

            // Spawn a per-request timeout: auto-deny if the user does not
            // respond within `permission_timeout_secs`.
            let pending = Arc::clone(&services.pending_perm_replies);
            let timeout_room_key = target_room.to_string();
            let timeout_request_id = perm_fwd.request_id.clone();
            let timeout_transport = Arc::clone(&transport);
            let timeout_room_str = target_room_str.clone();
            let timeout_sent_ids = Arc::clone(&bot_sent_event_ids);
            let timeout_secs = services.permission_timeout_secs;
            tokio::spawn(async move {
                tokio::time::sleep(Duration::from_secs(timeout_secs)).await;
                if let Some(tx) = pending
                    .remove_by_request_id(&timeout_room_key, &timeout_request_id)
                    .await
                {
                    let _ = tx.send(PermissionDecision::Deny);
                    let msg = "Permission request timed out — denied (fail-closed).";
                    let html = markdown_to_html(msg);
                    if let Ok(msg_id) = timeout_transport
                        .send_message(&timeout_room_str, msg, &html)
                        .await
                        && let Ok(event_id) = msg_id.parse::<OwnedEventId>()
                    {
                        timeout_sent_ids.lock().await.insert(event_id);
                    }
                }
            });
        }

        slog!("[matrix-bot] permission listener exiting (channel closed)");
    })
}

#[cfg(test)]
mod tests {
    //! Story 884 regression: spawn the listener with no in-flight chat
    //! message, send a PermissionForward through the channel, and verify
    //! the listener forwards it to the configured Matrix room (instead of
    //! being auto-denied for "no interactive session").

    use super::*;
    use crate::http::context::PermissionForward;
    use crate::service::permission_router::PendingPermReplies;
    use crate::services::Services;
    use async_trait::async_trait;
    use serde_json::json;
    use tokio::sync::oneshot;

    struct RecordingTransport {
        sent: Arc<std::sync::Mutex<Vec<(String, String)>>>,
    }

    #[async_trait]
    impl crate::chat::ChatTransport for RecordingTransport {
        async fn send_message(
            &self,
            room_id: &str,
            plain: &str,
            _html: &str,
        ) -> Result<crate::chat::MessageId, String> {
            self.sent
                .lock()
                .unwrap()
                .push((room_id.to_string(), plain.to_string()));
            // Matrix event IDs start with `$`; OwnedEventId::parse expects
            // a valid form so the listener can insert it into bot_sent_event_ids.
            Ok("$test_event_id:example.com".to_string())
        }

        async fn edit_message(&self, _: &str, _: &str, _: &str, _: &str) -> Result<(), String> {
            Ok(())
        }

        async fn send_typing(&self, _: &str, _: bool) -> Result<(), String> {
            Ok(())
        }
    }

    /// Poll `cond` until it returns `true` or `timeout` elapses, sleeping
    /// briefly between checks. Used instead of a single fixed sleep so tests
    /// don't flake under slow/loaded CI where a spawned task hasn't yet run.
    async fn wait_until(mut cond: impl FnMut() -> bool, timeout: std::time::Duration) -> bool {
        let start = tokio::time::Instant::now();
        loop {
            if cond() {
                return true;
            }
            if start.elapsed() > timeout {
                return false;
            }
            tokio::time::sleep(std::time::Duration::from_millis(5)).await;
        }
    }

    fn test_services() -> Arc<Services> {
        Arc::new(Services {
            project_root: std::path::PathBuf::from("/tmp/test"),
            agents: Arc::new(crate::agents::AgentPool::new_test(3000)),
            bot_name: "Assistant".to_string(),
            bot_user_id: "@bot:example.com".to_string(),
            ambient_rooms: Arc::new(std::sync::Mutex::new(std::collections::HashSet::new())),
            permission_registry: crate::service::permission_router::ResponderRegistry::new(),
            pending_perm_replies: PendingPermReplies::new(),
            permission_timeout_secs: 120,
            status: Arc::new(crate::service::status::StatusBroadcaster::new()),
            chat_dispatcher: Arc::new(crate::chat::dispatcher::ChatDispatcher::new(1_500)),
        })
    }

    #[tokio::test]
    async fn listener_forwards_request_to_target_room_when_no_chat_in_flight() {
        let services = test_services();
        let sent: Arc<std::sync::Mutex<Vec<(String, String)>>> =
            Arc::new(std::sync::Mutex::new(Vec::new()));
        let transport: Arc<dyn crate::chat::ChatTransport> = Arc::new(RecordingTransport {
            sent: Arc::clone(&sent),
        });
        let target_room: OwnedRoomId = "!perm:example.com".parse().unwrap();
        let bot_sent_event_ids = Arc::new(TokioMutex::new(HashSet::new()));

        spawn_permission_listener(
            Arc::clone(&services),
            Arc::clone(&transport),
            target_room.clone(),
            Arc::clone(&bot_sent_event_ids),
        );

        // Wait until the listener task registers and starts recv'ing.
        assert!(
            wait_until(
                || !services.permission_registry.is_empty(),
                std::time::Duration::from_secs(2)
            )
            .await,
            "listener never registered as a responder"
        );

        let (response_tx, _response_rx) = oneshot::channel();
        services.permission_registry.dispatch(PermissionForward {
            request_id: "req-1".to_string(),
            tool_name: "Bash".to_string(),
            tool_input: json!({"command": "cargo test"}),
            response_tx,
        });

        // Wait for the listener to process the forward.
        assert!(
            wait_until(
                || !sent.lock().unwrap().is_empty(),
                std::time::Duration::from_secs(2)
            )
            .await,
            "listener never sent the permission prompt"
        );

        // The transport must have received exactly one send_message to the
        // target room with the prompt content.
        let recorded = sent.lock().unwrap().clone();
        assert_eq!(recorded.len(), 1, "expected exactly one send_message");
        assert_eq!(recorded[0].0, target_room.as_str());
        assert!(
            recorded[0].1.contains("Permission Request"),
            "prompt body missing 'Permission Request' header: {}",
            recorded[0].1
        );
        assert!(
            recorded[0].1.contains("Bash"),
            "prompt body missing tool name: {}",
            recorded[0].1
        );

        // pending_perm_replies must contain an entry resolvable for the target
        // room (so the user-reply handler can resolve the request when they
        // reply). The insert happens just after send_message returns, so poll
        // briefly rather than assuming it's already visible.
        let mut resolved = None;
        for _ in 0..50 {
            resolved = services
                .pending_perm_replies
                .resolve_oldest(target_room.as_str())
                .await;
            if resolved.is_some() {
                break;
            }
            tokio::time::sleep(std::time::Duration::from_millis(5)).await;
        }
        assert!(
            resolved.is_some(),
            "pending_perm_replies missing entry for target room"
        );

        // bot_sent_event_ids must have recorded the prompt's event ID so the
        // bot does not echo its own prompt back as user input.
        assert!(
            wait_until(
                || bot_sent_event_ids
                    .try_lock()
                    .map(|s| s.len() == 1)
                    .unwrap_or(false),
                std::time::Duration::from_secs(2)
            )
            .await,
            "expected one sent event ID recorded"
        );
    }

    /// Regression: two concurrent permission requests for the same target
    /// room must both be resolvable — keying `pending_perm_replies` by
    /// request_id (rather than overwriting a single room-keyed entry) means
    /// the first request's oneshot sender is never dropped by the second.
    #[tokio::test]
    async fn two_concurrent_requests_for_same_room_both_resolve() {
        let services = test_services();
        let sent: Arc<std::sync::Mutex<Vec<(String, String)>>> =
            Arc::new(std::sync::Mutex::new(Vec::new()));
        let transport: Arc<dyn crate::chat::ChatTransport> = Arc::new(RecordingTransport {
            sent: Arc::clone(&sent),
        });
        let target_room: OwnedRoomId = "!perm:example.com".parse().unwrap();
        let bot_sent_event_ids = Arc::new(TokioMutex::new(HashSet::new()));

        spawn_permission_listener(
            Arc::clone(&services),
            Arc::clone(&transport),
            target_room.clone(),
            Arc::clone(&bot_sent_event_ids),
        );
        assert!(
            wait_until(
                || !services.permission_registry.is_empty(),
                std::time::Duration::from_secs(2)
            )
            .await,
            "listener never registered as a responder"
        );

        let (tx1, rx1) = oneshot::channel();
        services.permission_registry.dispatch(PermissionForward {
            request_id: "req-a".to_string(),
            tool_name: "Bash".to_string(),
            tool_input: json!({}),
            response_tx: tx1,
        });
        let (tx2, rx2) = oneshot::channel();
        services.permission_registry.dispatch(PermissionForward {
            request_id: "req-b".to_string(),
            tool_name: "Write".to_string(),
            tool_input: json!({}),
            response_tx: tx2,
        });

        // Wait until the listener has processed both forwards.
        assert!(
            wait_until(
                || sent.lock().unwrap().len() >= 2,
                std::time::Duration::from_secs(2)
            )
            .await,
            "listener never sent both permission prompts"
        );

        // Poll rather than assuming the pending_perm_replies insert (which
        // happens just after send_message returns) is already visible.
        let mut first = None;
        for _ in 0..50 {
            first = services
                .pending_perm_replies
                .resolve_oldest(target_room.as_str())
                .await;
            if first.is_some() {
                break;
            }
            tokio::time::sleep(std::time::Duration::from_millis(5)).await;
        }
        let first = first.expect("first request's sender must not have been dropped");
        let _ = first.send(PermissionDecision::Approve);
        assert_eq!(rx1.await.unwrap(), PermissionDecision::Approve);

        let mut second = None;
        for _ in 0..50 {
            second = services
                .pending_perm_replies
                .resolve_oldest(target_room.as_str())
                .await;
            if second.is_some() {
                break;
            }
            tokio::time::sleep(std::time::Duration::from_millis(5)).await;
        }
        let second = second.expect("second request's sender must still be present");
        let _ = second.send(PermissionDecision::Deny);
        assert_eq!(rx2.await.unwrap(), PermissionDecision::Deny);
    }
}
