use serde::Serialize;
use std::sync::{
    atomic::{AtomicU64, Ordering},
    Arc,
};

use crate::config::MatrixConfig;

#[derive(Clone)]
pub struct MatrixAppserviceClient {
    http: reqwest::Client,
    pub cfg: MatrixConfig,
    pub txn_counter: Arc<AtomicU64>,
}

impl MatrixAppserviceClient {
    pub fn new(http: reqwest::Client, cfg: MatrixConfig) -> Self {
        let start = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_millis() as u64;

        Self {
            http,
            cfg,
            txn_counter: Arc::new(AtomicU64::new(start)),
        }
    }

    /// Convert a node_id like "!deadbeef" into Matrix localpart "deadbeef".
    pub fn localpart_from_node_id(node_id: &str) -> String {
        node_id.trim_start_matches('!').to_string()
    }

    /// Build a full Matrix user_id from localpart.
    pub fn user_id(&self, localpart: &str) -> String {
        format!("@{}:{}", localpart, self.cfg.server_name)
    }

    fn auth_query(&self) -> String {
        format!("access_token={}", urlencoding::encode(&self.cfg.as_token))
    }

    /// Ensure the puppet user exists (register via appservice registration).
    pub async fn ensure_user_registered(&self, localpart: &str) -> anyhow::Result<()> {
        #[derive(Serialize)]
        struct RegisterReq<'a> {
            #[serde(rename = "type")]
            typ: &'a str,
            username: &'a str,
        }

        let url = format!(
            "{}/_matrix/client/v3/register?kind=user&{}",
            self.cfg.homeserver,
            self.auth_query()
        );

        let body = RegisterReq {
            typ: "m.login.application_service",
            username: localpart,
        };

        let resp = self.http.post(&url).json(&body).send().await?;
        if resp.status().is_success() {
            Ok(())
        } else {
            // If user already exists, Synapse / HS usually returns 400 M_USER_IN_USE.
            // We'll just ignore non-success and hope it's that case.
            Ok(())
        }
    }

    /// Set display name for puppet user.
    pub async fn set_display_name(&self, user_id: &str, display_name: &str) -> anyhow::Result<()> {
        #[derive(Serialize)]
        struct DisplayNameReq<'a> {
            displayname: &'a str,
        }

        let encoded_user = urlencoding::encode(user_id);
        let url = format!(
            "{}/_matrix/client/v3/profile/{}/displayname?user_id={}&{}",
            self.cfg.homeserver,
            encoded_user,
            encoded_user,
            self.auth_query()
        );

        let body = DisplayNameReq {
            displayname: display_name,
        };

        let resp = self.http.put(&url).json(&body).send().await?;
        if resp.status().is_success() {
            Ok(())
        } else {
            // Non-fatal.
            tracing::warn!(
                "Failed to set display name for {}: {}",
                user_id,
                resp.status()
            );
            Ok(())
        }
    }

    /// Send a plain text message into the configured room as puppet user_id.
    pub async fn send_text_message_as(&self, user_id: &str, body_text: &str) -> anyhow::Result<()> {
        #[derive(Serialize)]
        struct MsgContent<'a> {
            msgtype: &'a str,
            body: &'a str,
        }

        let txn_id = self.txn_counter.fetch_add(1, Ordering::SeqCst);
        let encoded_room = urlencoding::encode(&self.cfg.room_id);
        let encoded_user = urlencoding::encode(user_id);

        let url = format!(
            "{}/_matrix/client/v3/rooms/{}/send/m.room.message/{}?user_id={}&{}",
            self.cfg.homeserver,
            encoded_room,
            txn_id,
            encoded_user,
            self.auth_query()
        );

        let content = MsgContent {
            msgtype: "m.text",
            body: body_text,
        };

        let resp = self.http.put(&url).json(&content).send().await?;

        if !resp.status().is_success() {
            let status = resp.status();
            // optional: pull a short body snippet for debugging
            let body_snip = resp.text().await.unwrap_or_default();

            // Log for observability
            tracing::warn!(
                "Failed to send message as {}: status {}, body: {}",
                user_id,
                status,
                body_snip
            );

            // Propagate an error so callers know this message was NOT delivered
            return Err(anyhow::anyhow!(
                "Matrix send failed for {} with status {}",
                user_id,
                status
            ));
        }

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn dummy_cfg() -> MatrixConfig {
        MatrixConfig {
            homeserver: "https://matrix.example.org".to_string(),
            as_token: "AS_TOKEN".to_string(),
            server_name: "example.org".to_string(),
            room_id: "!roomid:example.org".to_string(),
        }
    }

    #[test]
    fn localpart_strips_bang_correctly() {
        assert_eq!(
            MatrixAppserviceClient::localpart_from_node_id("!deadbeef"),
            "deadbeef"
        );
        assert_eq!(
            MatrixAppserviceClient::localpart_from_node_id("cafebabe"),
            "cafebabe"
        );
    }

    #[test]
    fn user_id_builds_from_localpart_and_server_name() {
        let http = reqwest::Client::builder().build().unwrap();
        let client = MatrixAppserviceClient::new(http, dummy_cfg());

        let uid = client.user_id("deadbeef");
        assert_eq!(uid, "@deadbeef:example.org");
    }

    #[test]
    fn auth_query_contains_access_token() {
        let http = reqwest::Client::builder().build().unwrap();
        let client = MatrixAppserviceClient::new(http, dummy_cfg());

        let q = client.auth_query();
        assert!(q.starts_with("access_token="));
        assert!(q.contains("AS_TOKEN"));
    }

    #[test]
    fn test_new_matrix_client() {
        let http_client = reqwest::Client::new();
        let config = dummy_cfg();
        let client = MatrixAppserviceClient::new(http_client, config);
        assert_eq!(client.cfg.homeserver, "https://matrix.example.org");
        assert_eq!(client.cfg.as_token, "AS_TOKEN");
        assert!(client.txn_counter.load(Ordering::SeqCst) > 0);
    }

    #[tokio::test]
    async fn test_ensure_user_registered_success() {
        let mut server = mockito::Server::new_async().await;
        let mock = server
            .mock("POST", "/_matrix/client/v3/register")
            .match_query("kind=user&access_token=AS_TOKEN")
            .with_status(200)
            .create();

        let mut cfg = dummy_cfg();
        cfg.homeserver = server.url();
        let client = MatrixAppserviceClient::new(reqwest::Client::new(), cfg);
        let result = client.ensure_user_registered("testuser").await;

        mock.assert();
        assert!(result.is_ok());
    }

    #[tokio::test]
    async fn test_ensure_user_registered_user_in_use() {
        let mut server = mockito::Server::new_async().await;
        let mock = server
            .mock("POST", "/_matrix/client/v3/register")
            .match_query("kind=user&access_token=AS_TOKEN")
            .with_status(400) // M_USER_IN_USE
            .create();

        let mut cfg = dummy_cfg();
        cfg.homeserver = server.url();
        let client = MatrixAppserviceClient::new(reqwest::Client::new(), cfg);
        let result = client.ensure_user_registered("testuser").await;

        mock.assert();
        assert!(result.is_ok());
    }

    #[tokio::test]
    async fn test_set_display_name_success() {
        let mut server = mockito::Server::new_async().await;
        let user_id = "@test:example.org";
        let encoded_user = urlencoding::encode(user_id);
        let query = format!("user_id={}&access_token=AS_TOKEN", encoded_user);
        let path = format!("/_matrix/client/v3/profile/{}/displayname", encoded_user);

        let mock = server
            .mock("PUT", path.as_str())
            .match_query(query.as_str())
            .with_status(200)
            .create();

        let mut cfg = dummy_cfg();
        cfg.homeserver = server.url();
        let client = MatrixAppserviceClient::new(reqwest::Client::new(), cfg);
        let result = client.set_display_name(user_id, "Test Name").await;

        mock.assert();
        assert!(result.is_ok());
    }

    #[tokio::test]
    async fn test_set_display_name_fail_is_ok() {
        let mut server = mockito::Server::new_async().await;
        let user_id = "@test:example.org";
        let encoded_user = urlencoding::encode(user_id);
        let query = format!("user_id={}&access_token=AS_TOKEN", encoded_user);
        let path = format!("/_matrix/client/v3/profile/{}/displayname", encoded_user);

        let mock = server
            .mock("PUT", path.as_str())
            .match_query(query.as_str())
            .with_status(500)
            .create();

        let mut cfg = dummy_cfg();
        cfg.homeserver = server.url();
        let client = MatrixAppserviceClient::new(reqwest::Client::new(), cfg);
        let result = client.set_display_name(user_id, "Test Name").await;

        mock.assert();
        assert!(result.is_ok());
    }

    #[tokio::test]
    async fn test_send_text_message_as_success() {
        let mut server = mockito::Server::new_async().await;
        let user_id = "@test:example.org";
        let room_id = "!roomid:example.org";
        let encoded_user = urlencoding::encode(user_id);
        let encoded_room = urlencoding::encode(room_id);

        let client = {
            let mut cfg = dummy_cfg();
            cfg.homeserver = server.url();
            cfg.room_id = room_id.to_string();
            MatrixAppserviceClient::new(reqwest::Client::new(), cfg)
        };
        let txn_id = client.txn_counter.load(Ordering::SeqCst);
        let query = format!("user_id={}&access_token=AS_TOKEN", encoded_user);
        let path = format!(
            "/_matrix/client/v3/rooms/{}/send/m.room.message/{}",
            encoded_room, txn_id
        );

        let mock = server
            .mock("PUT", path.as_str())
            .match_query(query.as_str())
            .with_status(200)
            .create();

        let result = client.send_text_message_as(user_id, "hello").await;

        mock.assert();
        assert!(result.is_ok());
    }

    #[tokio::test]
    async fn test_send_text_message_as_fail() {
        let mut server = mockito::Server::new_async().await;
        let user_id = "@test:example.org";
        let room_id = "!roomid:example.org";
        let encoded_user = urlencoding::encode(user_id);
        let encoded_room = urlencoding::encode(room_id);

        let client = {
            let mut cfg = dummy_cfg();
            cfg.homeserver = server.url();
            cfg.room_id = room_id.to_string();
            MatrixAppserviceClient::new(reqwest::Client::new(), cfg)
        };
        let txn_id = client.txn_counter.load(Ordering::SeqCst);
        let query = format!("user_id={}&access_token=AS_TOKEN", encoded_user);
        let path = format!(
            "/_matrix/client/v3/rooms/{}/send/m.room.message/{}",
            encoded_room, txn_id
        );

        let mock = server
            .mock("PUT", path.as_str())
            .match_query(query.as_str())
            .with_status(500)
            .create();

        let result = client.send_text_message_as(user_id, "hello").await;

        mock.assert();
        assert!(result.is_err());
    }
}
