use std::{
    collections::HashMap,
    path::PathBuf,
    sync::Arc,
};

use matrix_sdk::{
    config::SyncSettings,
    ruma::{
        events::room::message::{MessageType, SyncRoomMessageEvent},
        OwnedUserId,
    },
    Client, Room,
};
use tokio::sync::RwLock;
use tracing::{debug, error, info, warn};

use crate::{config::MatrixBehaviorConfig, error::MatrixError, share::MatrixSessionShare, Result};

/// Key for the share registry: (connector_id, session_id)
type ShareKey = (String, String);

/// Central Matrix service.
///
/// Owns the SDK [`Client`], handles login/session-restore, manages the sync
/// loop, and maintains a registry of [`MatrixSessionShare`]s that bridge
/// Dirigent sessions to Matrix rooms.
pub struct MatrixService {
    account: dirigent_auth::Account,
    homeserver: String,
    username: String,
    display_name_str: String,
    device_id: String,
    behavior: MatrixBehaviorConfig,
    data_dir: PathBuf,
    client: Arc<RwLock<Option<Client>>>,
    shares: Arc<RwLock<HashMap<ShareKey, MatrixSessionShare>>>,
}

impl MatrixService {
    /// Create a new (not yet logged-in) service from an Account and behavior config.
    pub fn from_account(
        account: &dirigent_auth::Account,
        behavior: MatrixBehaviorConfig,
        data_dir: PathBuf,
    ) -> Result<Self> {
        let homeserver = account
            .property_str("homeserver")
            .ok_or_else(|| MatrixError::Config("Account missing 'homeserver' property".into()))?
            .to_string();
        let username = account
            .profile
            .username
            .clone()
            .ok_or_else(|| {
                MatrixError::Config("Account missing 'username' in profile".into())
            })?;
        let device_id = account
            .property_str_or("device_id", "DIRIGENT_01")
            .to_string();
        let display_name_str = account.display_name().to_string();

        Ok(Self {
            account: account.clone(),
            homeserver,
            username,
            display_name_str,
            device_id,
            behavior,
            data_dir,
            client: Arc::new(RwLock::new(None)),
            shares: Arc::new(RwLock::new(HashMap::new())),
        })
    }

    /// Return the current behavior configuration.
    pub fn behavior(&self) -> &MatrixBehaviorConfig {
        &self.behavior
    }

    /// Return a clone of the inner [`Client`], if logged in.
    pub async fn client_cloned(&self) -> Option<Client> {
        self.client.read().await.clone()
    }

    /// Resolve a Matrix [`Room`] handle from its string room id.
    ///
    /// Returns:
    /// - `Ok(Some(room))` — client is logged in and knows the room
    /// - `Ok(None)` — client is logged in but the room isn't known (not
    ///   joined / never invited / wrong id)
    /// - `Err(MatrixError::NotLoggedIn)` — no client yet
    /// - `Err(MatrixError::Config(..))` — `room_id` isn't a valid Matrix id
    ///
    /// Exposed for consumers (e.g. `dirigent_core`'s `MatrixFactory`)
    /// that need to look up a pre-existing room without taking a
    /// `matrix_sdk` dependency of their own.
    pub async fn room_by_id(&self, room_id: &str) -> Result<Option<Room>> {
        let client = self
            .client_cloned()
            .await
            .ok_or(MatrixError::NotLoggedIn)?;

        let parsed: matrix_sdk::ruma::OwnedRoomId = room_id
            .parse()
            .map_err(|e: matrix_sdk::ruma::IdParseError| {
                MatrixError::Config(format!("invalid room_id '{}': {}", room_id, e))
            })?;

        Ok(client.get_room(&parsed))
    }

    // -----------------------------------------------------------------------
    // Authentication
    // -----------------------------------------------------------------------

    /// Build the SDK client (with SQLite store) and authenticate.
    ///
    /// Attempts to restore a previously persisted session first.  Falls back
    /// to a fresh username/password login when no session is found.
    pub async fn login(&self) -> Result<()> {
        let store_path = self.data_dir.join(&self.behavior.store_path);
        std::fs::create_dir_all(&store_path).map_err(|e| {
            MatrixError::Config(format!("Failed to create store directory: {}", e))
        })?;

        let client = Client::builder()
            .homeserver_url(&self.homeserver)
            .sqlite_store(&store_path, None)
            .build()
            .await?;

        // Try to restore an existing session from the store.
        if client.logged_in() {
            info!(
                homeserver = %self.homeserver,
                username = %self.username,
                "Restored existing Matrix session"
            );
            *self.client.write().await = Some(client);
            return Ok(());
        }

        // No stored session — authenticate based on the configured mode.
        match self.behavior.mode {
            crate::config::MatrixConnectionMode::Bot => {
                // Existing flow: login with bot username/password.
                let password = self
                    .account
                    .resolve_credential("password")
                    .map_err(|e| {
                        MatrixError::Config(format!("Failed to resolve password: {}", e))
                    })?;

                info!(
                    homeserver = %self.homeserver,
                    username = %self.username,
                    "Performing fresh Matrix login (bot mode)"
                );

                client
                    .matrix_auth()
                    .login_username(&self.username, &password)
                    .device_id(&self.device_id)
                    .initial_device_display_name(&self.display_name_str)
                    .send()
                    .await?;
            }
            crate::config::MatrixConnectionMode::Provisioned => {
                // Restore session from a stored virtual-user access token.
                let token = self
                    .account
                    .resolve_credential("token")
                    .map_err(|e| {
                        MatrixError::Config(format!("Failed to resolve token: {}", e))
                    })?;

                let user_id_str = self
                    .account
                    .property_str("user_id")
                    .ok_or_else(|| {
                        MatrixError::Config(
                            "Provisioned mode requires 'user_id' property on account".into(),
                        )
                    })?;

                info!(
                    homeserver = %self.homeserver,
                    user_id = %user_id_str,
                    "Restoring provisioned session with access token"
                );

                use matrix_sdk::matrix_auth::MatrixSession;
                use matrix_sdk::ruma::{OwnedDeviceId, OwnedUserId};

                let user_id: OwnedUserId = user_id_str.try_into().map_err(|_| {
                    MatrixError::Config(format!("Invalid user_id: {}", user_id_str))
                })?;
                let device_id: OwnedDeviceId = self.device_id.clone().into();

                let session = MatrixSession {
                    meta: matrix_sdk::SessionMeta {
                        user_id,
                        device_id,
                    },
                    tokens: matrix_sdk::matrix_auth::MatrixSessionTokens {
                        access_token: token,
                        refresh_token: None,
                    },
                };

                client.matrix_auth().restore_session(session).await?;
            }
        }

        info!(
            homeserver = %self.homeserver,
            username = %self.username,
            "Matrix login successful"
        );

        *self.client.write().await = Some(client);
        Ok(())
    }

    // -----------------------------------------------------------------------
    // Sync loop
    // -----------------------------------------------------------------------

    /// Start the background sync task.
    ///
    /// Registers an event handler for incoming room messages and spawns a
    /// task that runs `client.sync()` indefinitely.  Returns immediately
    /// after spawning.
    ///
    /// # Errors
    ///
    /// Returns [`MatrixError::NotLoggedIn`] if `login()` was not called first.
    pub async fn start_sync(&self) -> Result<()> {
        let client = self
            .client
            .read()
            .await
            .clone()
            .ok_or(MatrixError::NotLoggedIn)?;

        if !client.logged_in() {
            return Err(MatrixError::NotLoggedIn);
        }

        let shares = Arc::clone(&self.shares);
        let bot_user_id: Option<OwnedUserId> = client.user_id().map(|u| u.to_owned());

        // Register the room message event handler.
        client.add_event_handler({
            let shares = Arc::clone(&shares);
            let bot_user_id = bot_user_id.clone();
            move |ev: SyncRoomMessageEvent, room: Room| {
                let shares = Arc::clone(&shares);
                let bot_user_id = bot_user_id.clone();
                async move {
                    on_room_message(ev, room, shares, bot_user_id).await;
                }
            }
        });

        // Spawn the sync loop.
        tokio::spawn(async move {
            info!("Starting Matrix sync loop");
            if let Err(e) = client.sync(SyncSettings::default()).await {
                error!("Matrix sync loop exited with error: {}", e);
            }
        });

        Ok(())
    }

    // -----------------------------------------------------------------------
    // Share registry
    // -----------------------------------------------------------------------

    /// Register a share, making it eligible to receive Matrix messages.
    ///
    /// # Errors
    ///
    /// Returns [`MatrixError::ShareAlreadyExists`] if a share for the same
    /// `(connector_id, session_id)` pair is already registered.
    pub async fn register_share(&self, share: MatrixSessionShare) -> Result<()> {
        let key = (share.connector_id.clone(), share.session_id.clone());
        let mut map = self.shares.write().await;
        if map.contains_key(&key) {
            return Err(MatrixError::ShareAlreadyExists {
                connector_id: key.0,
                session_id: key.1,
            });
        }
        map.insert(key, share);
        Ok(())
    }

    /// Remove a share from the registry and shut it down.
    ///
    /// # Errors
    ///
    /// Returns [`MatrixError::ShareNotFound`] if no matching share exists.
    pub async fn remove_share(&self, connector_id: &str, session_id: &str) -> Result<()> {
        let key = (connector_id.to_owned(), session_id.to_owned());
        let share = self
            .shares
            .write()
            .await
            .remove(&key)
            .ok_or_else(|| MatrixError::ShareNotFound {
                connector_id: connector_id.to_owned(),
                session_id: session_id.to_owned(),
            })?;
        share.shutdown().await;
        Ok(())
    }

    /// Return the number of currently registered shares.
    pub async fn share_count(&self) -> usize {
        self.shares.read().await.len()
    }

    /// Return the room IDs and keys for all currently registered shares.
    ///
    /// Returns a list of `(connector_id, session_id, room_id)` tuples.
    pub async fn list_shares(&self) -> Vec<(String, String, String)> {
        self.shares
            .read()
            .await
            .iter()
            .map(|((cid, sid), s)| (cid.clone(), sid.clone(), s.room_id.clone()))
            .collect()
    }

    /// Look up a share by connector and session ID.
    ///
    /// Returns `None` when not found.
    pub async fn get_share(
        &self,
        connector_id: &str,
        session_id: &str,
    ) -> Option<(String, bool)> {
        let key = (connector_id.to_owned(), session_id.to_owned());
        let map = self.shares.read().await;
        if let Some(share) = map.get(&key) {
            let room_id = share.room_id.clone();
            // is_active requires an await; we can't hold the read guard across
            // the await point.  Drop the guard first.
            drop(map);
            let active = {
                // Re-acquire to call is_active (we still have shares Arc)
                let map = self.shares.read().await;
                if let Some(s) = map.get(&key) {
                    s.is_active().await
                } else {
                    false
                }
            };
            Some((room_id, active))
        } else {
            None
        }
    }

    /// Shut down all shares and signal the service to stop.
    pub async fn shutdown(&self) {
        let mut map = self.shares.write().await;
        for (_, share) in map.drain() {
            share.shutdown().await;
        }
        // Release the client so the sync loop can terminate naturally.
        *self.client.write().await = None;
    }
}

// ---------------------------------------------------------------------------
// Event handler: Matrix → Dirigent
// ---------------------------------------------------------------------------

/// Called by the SDK for every incoming room message.
///
/// Looks up which registered share owns this room, then calls
/// `share.inject_message(text)` so the text flows into the Dirigent session.
/// Messages from the bot itself are skipped.
async fn on_room_message(
    ev: SyncRoomMessageEvent,
    room: Room,
    shares: Arc<RwLock<HashMap<ShareKey, MatrixSessionShare>>>,
    bot_user_id: Option<OwnedUserId>,
) {
    // We only care about original (non-redacted, non-edited) messages.
    let original = match ev.as_original() {
        Some(o) => o,
        None => return,
    };

    // Skip bot's own messages.
    if let Some(bot_id) = &bot_user_id {
        if original.sender == *bot_id {
            return;
        }
    }

    // Extract plain-text body.
    let text = match &original.content.msgtype {
        MessageType::Text(t) => t.body.clone(),
        MessageType::Notice(_) => return, // ignore notices (including our own)
        _ => return,
    };

    let room_id_str = room.room_id().as_str().to_owned();

    // Find the share that owns this room.
    let map = shares.read().await;
    let matching = map
        .values()
        .find(|s| s.room_id == room_id_str);

    if let Some(share) = matching {
        debug!(
            room_id = %room_id_str,
            connector_id = %share.connector_id,
            session_id = %share.session_id,
            "Injecting Matrix message into Dirigent session"
        );
        share.inject_message(&text).await;
    } else {
        warn!(
            room_id = %room_id_str,
            "Received message in unregistered room, ignoring"
        );
    }
}
