//! `MatrixFactory`: build a Matrix [`SessionStream`] from a `[[streams]]`
//! config block.
//!
//! This is the first stream-side factory wired for the Phase 4 migration
//! (Task 18). The factory lives in `dirigent_core` rather than
//! `dirigent_matrix` because `StreamFactory` is defined here and
//! `dirigent_core` already depends on `dirigent_matrix` — putting it in
//! `dirigent_matrix` would create a cycle.
//!
//! ## Scope
//!
//! The factory's responsibility is narrow: parse `cfg.params`, resolve
//! the target Matrix room via a running `MatrixService`, and construct a
//! `MatrixSessionShare` configured for the stream path (no legacy
//! forwarder task). Command-proxy wiring (Matrix → Dirigent
//! `ConnectorCommand::SendMessage`) remains in
//! `CoreRuntime::create_matrix_share` for now; a follow-up will extend
//! the factory to cover that path.
//!
//! ## Config shape
//!
//! ```toml
//! [[streams]]
//! name = "matrix-main"
//! type = "matrix"
//!
//! [streams.scope]
//! kind = "session"
//! scroll_id = "01985d00-..."
//!
//! [streams.params]
//! connector_id    = "opencode-1"       # dirigent connector key
//! session_id      = "native-abc123"    # native connector session id
//! room_id         = "!abc:matrix.org"  # pre-existing room to attach to
//! homeserver_url  = "https://matrix.org" # informational (service already knows)
//! ```

use std::sync::Arc;

use async_trait::async_trait;
use serde::Deserialize;

use dirigent_protocol::streaming::{SessionStream, StreamScope};

use super::config::StreamConfig;
use super::factory::{StreamBuildError, StreamFactory};

/// Stream-side factory for Matrix. See module docs for the expected
/// TOML shape.
pub struct MatrixFactory {
    service: Arc<dirigent_matrix::MatrixService>,
}

impl MatrixFactory {
    /// Build a factory bound to a running `MatrixService`. The service
    /// is expected to be logged in and sync-started by the time
    /// `build()` is called; if it isn't, `build()` returns
    /// `StreamBuildError::Transport`.
    pub fn new(service: Arc<dirigent_matrix::MatrixService>) -> Self {
        Self { service }
    }
}

#[derive(Debug, Deserialize)]
struct MatrixStreamParams {
    /// Dirigent connector id that owns the session being bridged.
    connector_id: String,
    /// Native connector session id.
    session_id: String,
    /// Matrix room id — must be a pre-existing room the bot can access.
    /// Room creation is still handled by
    /// `CoreRuntime::create_matrix_share` until the factory path is
    /// expanded to cover it.
    room_id: String,
    /// Informational; the logged-in `MatrixService` is the authority on
    /// which homeserver to talk to. Accepted so configs can be
    /// self-documenting and round-trip through TOML.
    #[serde(default)]
    #[allow(dead_code)]
    homeserver_url: Option<String>,
}

#[async_trait]
impl StreamFactory for MatrixFactory {
    fn kind(&self) -> &'static str {
        "matrix"
    }

    async fn build(
        &self,
        cfg: &StreamConfig,
    ) -> Result<Arc<dyn SessionStream>, StreamBuildError> {
        // Scope must be Session; Matrix shares are intrinsically
        // per-session bi-directional bridges.
        let scroll_id = match &cfg.scope {
            StreamScope::Session { scroll_id } => *scroll_id,
            other => {
                return Err(StreamBuildError::Config(format!(
                    "matrix stream requires scope.kind = \"session\", got {:?}",
                    other
                )));
            }
        };

        // Parse type-specific params.
        let params: MatrixStreamParams = cfg
            .params
            .clone()
            .try_into()
            .map_err(|e: toml::de::Error| {
                StreamBuildError::Config(format!(
                    "matrix stream '{}': invalid params: {}",
                    cfg.name, e
                ))
            })?;

        // Look up the room via the service. We intentionally don't
        // create or join rooms here — the room must already exist.
        // Creation remains the responsibility of
        // `CoreRuntime::create_matrix_share`.
        let room = match self.service.room_by_id(&params.room_id).await {
            Ok(Some(room)) => room,
            Ok(None) => {
                return Err(StreamBuildError::Transport(format!(
                    "matrix stream '{}': room '{}' not found on client \
                     — ensure the bot has joined it",
                    cfg.name, params.room_id
                )));
            }
            Err(dirigent_matrix::MatrixError::NotLoggedIn) => {
                return Err(StreamBuildError::Transport(
                    "matrix service is not logged in; cannot build stream"
                        .to_string(),
                ));
            }
            Err(dirigent_matrix::MatrixError::Config(msg)) => {
                return Err(StreamBuildError::Config(format!(
                    "matrix stream '{}': {}",
                    cfg.name, msg
                )));
            }
            Err(other) => {
                return Err(StreamBuildError::Transport(format!(
                    "matrix stream '{}': {}",
                    cfg.name, other
                )));
            }
        };

        // Construct the share for the stream path (no legacy forwarder
        // task). We drop the command receiver on the floor here — the
        // Matrix → Dirigent direction is not covered by this factory
        // yet; see the follow-up TODO in the module docs.
        let (share, _command_rx) = dirigent_matrix::MatrixSessionShare::new_for_stream(
            params.connector_id,
            params.session_id,
            scroll_id,
            params.room_id,
            room,
        );

        Ok(Arc::new(share) as Arc<dyn SessionStream>)
    }
}

// ─── Tests ───────────────────────────────────────────────────────────────────

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

    #[test]
    fn factory_kind_is_matrix() {
        // The factory's `kind()` is static and doesn't require a running
        // MatrixService to read — covered by a minimal construction
        // check in the integration test suite.
        fn assert_is_factory<F: StreamFactory>(_: &F) {}

        // We can't easily build a MatrixService in a unit test (it needs
        // an Account + data dir + SQLite store). The full smoke test
        // lives in `packages/dirigent_matrix/tests/factory_test.rs` and
        // the cross-crate registry test in
        // `packages/dirigent_core/tests/matrix_migration_test.rs`.
        //
        // This module-local test exists only to assert that the impl
        // block type-checks against the `StreamFactory` trait bound.
        fn _compile_check(f: &MatrixFactory) {
            assert_is_factory(f);
        }
    }

    #[test]
    fn matrix_stream_params_deserialise_ok() {
        let toml_str = r#"
connector_id = "opencode-1"
session_id = "native-abc"
room_id = "!foo:example.com"
homeserver_url = "https://matrix.org"
"#;
        let p: MatrixStreamParams = toml::from_str(toml_str).expect("parse");
        assert_eq!(p.connector_id, "opencode-1");
        assert_eq!(p.session_id, "native-abc");
        assert_eq!(p.room_id, "!foo:example.com");
        assert_eq!(p.homeserver_url.as_deref(), Some("https://matrix.org"));
    }

    #[test]
    fn matrix_stream_params_reject_missing_required() {
        // Missing room_id should fail.
        let toml_str = r#"
connector_id = "opencode-1"
session_id = "native-abc"
"#;
        let err: Result<MatrixStreamParams, _> = toml::from_str(toml_str);
        assert!(err.is_err());
    }
}
