//! Host-level traits and remote host handles.
//!
//! A host represents one physical Enody product. It owns fixtures and exposes
//! product-level operations such as network scanning and WiFi joining.

use alloc::boxed::Box;

use crate::{fixture::Fixture, message::Version, Identifier};

/// Represents a host device that can provide access to fixtures.
///
/// A host is typically a physical device (like EP01) that contains
/// one or more fixtures.
pub trait Host: Send + Sync {
    /// Returns the stable host identifier.
    fn identifier(&self) -> Identifier;

    /// Returns the firmware version reported by the host.
    fn version(&self) -> Version;

    /// Returns the fixtures owned by this host.
    fn fixtures(&self) -> &[Box<dyn Fixture>];
}

#[cfg(feature = "remote")]
/// Remote host handles.
pub mod remote {
    use crate::{
        fixture::remote::RemoteFixture,
        message::{
            Command, CommandMessage, Event, FixtureInfo, HostCommand, HostEvent, HostInfo, Network,
            NetworkCredentials, Version, WifiCredentials, WifiNetwork, NETWORK_SCAN_FILTER_MAX_LEN,
            NETWORK_SCAN_RESULT_MAX_LEN,
        },
        runtime::remote::RemoteRuntime,
        Identifier,
    };
    use heapless::Vec as HVec;

    /// A host accessed via remote runtime communication.
    ///
    /// RemoteHost wraps a cloned RemoteRuntime and provides high-level access
    /// to host information and fixtures through the command/event protocol.
    /// Since RemoteRuntime implements Clone with a shared internal connection,
    /// multiple RemoteHosts can share the same underlying connection.
    #[derive(Debug, Clone)]
    pub struct RemoteHost {
        info: HostInfo,
        remote: RemoteRuntime,
    }

    impl RemoteHost {
        /// Create a RemoteHost by querying the device for its host information.
        pub async fn from_runtime(remote: RemoteRuntime) -> Result<Self, crate::Error> {
            let info = Self::fetch_host_info(&remote).await?;
            Ok(Self::new(info, remote))
        }

        /// Fetch host information from the remote device.
        async fn fetch_host_info(remote: &RemoteRuntime) -> Result<HostInfo, crate::Error> {
            let command = Command::Host(HostCommand::Info);
            let command_message = CommandMessage::root(command, None);

            let event_message = remote.execute_command(command_message).await?;

            match event_message.event {
                Event::Host(HostEvent::Info(host_info)) => Ok(host_info),
                _ => Err(crate::Error::UnexpectedResponse),
            }
        }

        /// Create a new RemoteHost with the given runtime and host info.
        pub fn new(info: HostInfo, remote: RemoteRuntime) -> Self {
            Self { info, remote }
        }

        /// Get the host info.
        pub fn info(&self) -> &HostInfo {
            &self.info
        }

        /// Get the host identifier.
        pub fn identifier(&self) -> Identifier {
            self.info.identifier
        }

        /// Get the host version.
        pub fn version(&self) -> Version {
            self.info.version.clone()
        }

        /// Fetch the number of fixtures from the remote device.
        async fn fixture_count(&self) -> Result<u32, crate::Error> {
            let command = Command::Host(HostCommand::FixtureCount);
            let command_message = CommandMessage::root(command, Some(self.identifier()));

            let event_message = self.remote.execute_command(command_message).await?;

            match event_message.event {
                Event::Host(HostEvent::FixtureCount(count)) => Ok(count),
                _ => Err(crate::Error::UnexpectedResponse),
            }
        }

        /// Fetch information about a specific fixture by index.
        async fn fixture_info(&self, index: u32) -> Result<FixtureInfo, crate::Error> {
            let command = Command::Host(HostCommand::FixtureInfo(index));
            let command_message = CommandMessage::root(command, Some(self.identifier()));

            let event_message = self.remote.execute_command(command_message).await?;

            match event_message.event {
                Event::Host(HostEvent::FixtureInfo(info)) => Ok(info),
                _ => Err(crate::Error::UnexpectedResponse),
            }
        }

        /// Discover and create RemoteFixture objects for all fixtures on this host.
        pub async fn fixtures(&self) -> Result<Vec<RemoteFixture>, crate::Error> {
            let count = self.fixture_count().await?;
            let mut fixtures = Vec::with_capacity(count as usize);

            for i in 0..count {
                let info = self.fixture_info(i).await?;
                let fixture = RemoteFixture::new(info, self.remote.clone());
                fixtures.push(fixture);
            }

            Ok(fixtures)
        }

        /// Scan networks matching the provided filters.
        ///
        /// The result list is bounded by [`NETWORK_SCAN_RESULT_MAX_LEN`].
        pub async fn network_scan(
            &self,
            filters: HVec<Network, NETWORK_SCAN_FILTER_MAX_LEN>,
        ) -> Result<HVec<Network, NETWORK_SCAN_RESULT_MAX_LEN>, crate::Error> {
            let command = Command::Host(HostCommand::NetworkScan(filters));
            let message = self
                .remote
                .execute_command_with_timeout_until(
                    CommandMessage::root(command, Some(self.identifier())),
                    std::time::Duration::from_secs(10),
                    |message| {
                        matches!(
                            message.event,
                            Event::Host(HostEvent::NetworkScanComplete(_))
                        )
                    },
                )
                .await?;
            match message.event {
                Event::Host(HostEvent::NetworkScanComplete(results)) => Ok(results),
                _ => Err(crate::Error::UnexpectedResponse),
            }
        }

        /// Join a network using the provided credentials.
        ///
        /// This sends credentials to the product over the current trusted
        /// runtime connection and waits for network join completion.
        pub async fn network_join(
            &self,
            network: Network,
            credentials: NetworkCredentials,
        ) -> Result<(), crate::Error> {
            let command = Command::Host(HostCommand::NetworkJoin(network, credentials));
            let message = self
                .remote
                .execute_command_with_timeout_until(
                    CommandMessage::root(command, Some(self.identifier())),
                    std::time::Duration::from_secs(10),
                    |message| {
                        matches!(
                            message.event,
                            Event::Host(HostEvent::NetworkJoinComplete(_))
                        )
                    },
                )
                .await?;
            match message.event {
                Event::Host(HostEvent::NetworkJoinComplete(_)) => Ok(()),
                _ => Err(crate::Error::UnexpectedResponse),
            }
        }

        /// Scan for nearby WiFi networks.
        pub async fn wifi_scan(
            &self,
        ) -> Result<HVec<Network, NETWORK_SCAN_RESULT_MAX_LEN>, crate::Error> {
            let mut filters = HVec::new();
            filters
                .push(Network::Wifi(WifiNetwork::default()))
                .map_err(|_| crate::Error::Argument)?;
            self.network_scan(filters).await
        }

        /// Join a WiFi network by SSID and password.
        ///
        /// Pass an empty password for an open network.
        pub async fn wifi_join(&self, ssid: &str, password: &str) -> Result<(), crate::Error> {
            let network = Network::Wifi(WifiNetwork {
                ssid: Some(heapless::String::try_from(ssid).map_err(|_| crate::Error::Argument)?),
                ..WifiNetwork::default()
            });
            let credentials = if password.is_empty() {
                NetworkCredentials::None
            } else {
                NetworkCredentials::Wifi(WifiCredentials::Password(
                    heapless::String::try_from(password).map_err(|_| crate::Error::Argument)?,
                ))
            };
            self.network_join(network, credentials).await
        }
    }
}
