//! `QpqClient` — the main entry point for the quicproquo SDK.

use std::sync::Arc;

use tokio::sync::broadcast;
use tracing::info;

use crate::config::ClientConfig;
use crate::conversation::ConversationStore;
use crate::error::SdkError;
use crate::events::ClientEvent;

/// The main SDK client. All state is contained within this struct — no globals.
pub struct QpqClient {
    config: ClientConfig,
    rpc: Option<quicproquo_rpc::client::RpcClient>,
    event_tx: broadcast::Sender<ClientEvent>,
    /// The authenticated username, if logged in.
    username: Option<String>,
    /// The local identity key (Ed25519 public key, 32 bytes).
    identity_key: Option<Vec<u8>>,
    /// Session token from OPAQUE login.
    session_token: Option<Vec<u8>>,
    /// Local conversation store (SQLCipher).
    conv_store: Option<ConversationStore>,
    /// Device ID for multi-device support.
    /// When set, fetch/peek/ack requests include this device_id so the server
    /// scopes them to the correct per-device queue.
    device_id: Option<Vec<u8>>,
}

impl QpqClient {
    /// Create a new client with the given configuration.
    pub fn new(config: ClientConfig) -> Self {
        let (event_tx, _) = broadcast::channel(256);
        Self {
            config,
            rpc: None,
            event_tx,
            username: None,
            identity_key: None,
            session_token: None,
            conv_store: None,
            device_id: None,
        }
    }

    /// Connect to the server.
    pub async fn connect(&mut self) -> Result<(), SdkError> {
        let tls_config = build_tls_config(self.config.accept_invalid_certs)?;

        let rpc_config = quicproquo_rpc::client::RpcClientConfig {
            server_addr: self.config.server_addr,
            server_name: self.config.server_name.clone(),
            tls_config: Arc::new(tls_config),
            alpn: self.config.alpn.clone(),
            session_token: self.session_token.clone(),
        };

        let client = quicproquo_rpc::client::RpcClient::connect(rpc_config).await?;
        self.rpc = Some(client);

        // Open local conversation store.
        let store = ConversationStore::open(
            &self.config.db_path,
            self.config.db_password.as_deref(),
        )?;
        self.conv_store = Some(store);

        self.emit(ClientEvent::Connected);
        info!(server = %self.config.server_addr, "connected");
        Ok(())
    }

    /// Subscribe to client events. Returns a broadcast receiver.
    pub fn subscribe(&self) -> broadcast::Receiver<ClientEvent> {
        self.event_tx.subscribe()
    }

    /// Get the authenticated username, if logged in.
    pub fn username(&self) -> Option<&str> {
        self.username.as_deref()
    }

    /// Get the local identity key.
    pub fn identity_key(&self) -> Option<&[u8]> {
        self.identity_key.as_deref()
    }

    /// Whether the client is connected.
    pub fn is_connected(&self) -> bool {
        self.rpc.is_some()
    }

    /// Whether the client is authenticated.
    pub fn is_authenticated(&self) -> bool {
        self.session_token.is_some()
    }

    /// Get the server address as a string (e.g. "127.0.0.1:7000").
    pub fn server_addr_string(&self) -> String {
        self.config.server_addr.to_string()
    }

    /// Get the state file path from the client configuration.
    pub fn config_state_path(&self) -> std::path::PathBuf {
        self.config.state_path.clone()
    }

    /// Get a reference to the RPC client (for direct calls).
    pub fn rpc(&self) -> Result<&quicproquo_rpc::client::RpcClient, SdkError> {
        self.rpc.as_ref().ok_or(SdkError::NotConnected)
    }

    /// Get a reference to the conversation store.
    pub fn conversations(&self) -> Result<&ConversationStore, SdkError> {
        self.conv_store
            .as_ref()
            .ok_or(SdkError::NotConnected)
    }

    /// Register a new user account via OPAQUE.
    ///
    /// Generates a fresh identity keypair, registers it with the server, and
    /// stores the identity key locally.
    pub async fn register(&mut self, username: &str, password: &str) -> Result<(), SdkError> {
        let rpc = self.rpc.as_ref().ok_or(SdkError::NotConnected)?;
        let keypair = crate::auth::opaque_register(rpc, username, password, None).await?;
        self.identity_key = Some(keypair.public_key_bytes().to_vec());
        self.emit(ClientEvent::Registered {
            username: username.to_string(),
        });
        info!(username, "registered");
        Ok(())
    }

    /// Log in via OPAQUE and store the session token.
    ///
    /// Requires an identity key to be set (either from a previous `register()`
    /// call or loaded from state). After login, the client is authenticated
    /// and subsequent RPC calls include the session token.
    pub async fn login(&mut self, username: &str, password: &str) -> Result<(), SdkError> {
        let identity_key = self
            .identity_key
            .as_ref()
            .ok_or_else(|| SdkError::AuthFailed("no identity key — register or load state first".into()))?
            .clone();

        let rpc = self.rpc.as_ref().ok_or(SdkError::NotConnected)?;
        let session_token = crate::auth::opaque_login(rpc, username, password, &identity_key).await?;

        self.session_token = Some(session_token);
        self.username = Some(username.to_string());
        self.emit(ClientEvent::LoggedIn {
            username: username.to_string(),
        });
        info!(username, "logged in");
        Ok(())
    }

    /// Clear authentication state (session token, username).
    pub fn logout(&mut self) -> Result<(), SdkError> {
        self.session_token = None;
        let username = self.username.take();
        self.emit(ClientEvent::LoggedOut {
            username: username.unwrap_or_default(),
        });
        info!("logged out");
        Ok(())
    }

    /// Set the identity key directly (e.g. after loading from state).
    pub fn set_identity_key(&mut self, key: Vec<u8>) {
        self.identity_key = Some(key);
    }

    /// Get the session token, if authenticated.
    pub fn session_token(&self) -> Option<&[u8]> {
        self.session_token.as_deref()
    }

    // ── Multi-device ─────────────────────────────────────────────────────────

    /// Set the device ID for this client. Subsequent fetch/peek/ack calls
    /// will include this ID so the server scopes them to the correct queue.
    pub fn set_device_id(&mut self, device_id: Vec<u8>) {
        self.device_id = Some(device_id);
    }

    /// Get the current device ID, if set.
    pub fn device_id(&self) -> Option<&[u8]> {
        self.device_id.as_deref()
    }

    /// Register this device with the server.
    /// Sets the local device_id on success.
    pub async fn register_device(
        &mut self,
        device_id: &[u8],
        device_name: &str,
    ) -> Result<bool, SdkError> {
        let rpc = self.rpc.as_ref().ok_or(SdkError::NotConnected)?;
        let newly_registered =
            crate::devices::register_device(rpc, device_id, device_name).await?;
        self.device_id = Some(device_id.to_vec());
        Ok(newly_registered)
    }

    /// List all registered devices for this identity.
    pub async fn list_devices(&self) -> Result<Vec<crate::devices::DeviceInfo>, SdkError> {
        let rpc = self.rpc.as_ref().ok_or(SdkError::NotConnected)?;
        crate::devices::list_devices(rpc).await
    }

    /// Revoke (remove) a registered device.
    pub async fn revoke_device(&self, device_id: &[u8]) -> Result<bool, SdkError> {
        let rpc = self.rpc.as_ref().ok_or(SdkError::NotConnected)?;
        crate::devices::revoke_device(rpc, device_id).await
    }

    // ── Moderation (client-side) ────────────────────────────────────────────

    /// Block a user locally. Their messages will be hidden from display.
    pub fn block_user(&self, identity_key: &[u8], reason: &str) -> Result<(), SdkError> {
        let store = self.conversations()?;
        store
            .block_user(identity_key, reason)
            .map_err(|e| SdkError::Storage(e.to_string()))?;
        info!(identity = %hex::encode(identity_key), "user blocked");
        Ok(())
    }

    /// Unblock a user locally.
    pub fn unblock_user(&self, identity_key: &[u8]) -> Result<bool, SdkError> {
        let store = self.conversations()?;
        let removed = store
            .unblock_user(identity_key)
            .map_err(|e| SdkError::Storage(e.to_string()))?;
        if removed {
            info!(identity = %hex::encode(identity_key), "user unblocked");
        }
        Ok(removed)
    }

    /// Check if a user is blocked locally.
    pub fn is_blocked(&self, identity_key: &[u8]) -> Result<bool, SdkError> {
        let store = self.conversations()?;
        store
            .is_blocked(identity_key)
            .map_err(|e| SdkError::Storage(e.to_string()))
    }

    /// List all locally blocked users.
    pub fn list_blocked(&self) -> Result<Vec<crate::conversation::BlockedUser>, SdkError> {
        let store = self.conversations()?;
        store
            .list_blocked()
            .map_err(|e| SdkError::Storage(e.to_string()))
    }

    /// Disconnect from the server.
    pub fn disconnect(&mut self) {
        if let Some(rpc) = self.rpc.take() {
            rpc.close();
            self.emit(ClientEvent::Disconnected {
                reason: "client closed".into(),
            });
        }
    }

    fn emit(&self, event: ClientEvent) {
        // Ignore send errors (no subscribers).
        let _ = self.event_tx.send(event);
    }
}

impl Drop for QpqClient {
    fn drop(&mut self) {
        self.disconnect();
    }
}

fn build_tls_config(accept_invalid_certs: bool) -> Result<rustls::ClientConfig, SdkError> {
    let builder = rustls::ClientConfig::builder();

    if accept_invalid_certs {
        let config = builder
            .dangerous()
            .with_custom_certificate_verifier(Arc::new(InsecureVerifier))
            .with_no_client_auth();
        Ok(config)
    } else {
        let roots = rustls::RootCertStore::empty();
        let config = builder
            .with_root_certificates(roots)
            .with_no_client_auth();
        Ok(config)
    }
}

/// A TLS verifier that accepts any certificate (for dev mode only).
#[derive(Debug)]
struct InsecureVerifier;

impl rustls::client::danger::ServerCertVerifier for InsecureVerifier {
    fn verify_server_cert(
        &self,
        _end_entity: &rustls::pki_types::CertificateDer<'_>,
        _intermediates: &[rustls::pki_types::CertificateDer<'_>],
        _server_name: &rustls::pki_types::ServerName<'_>,
        _ocsp_response: &[u8],
        _now: rustls::pki_types::UnixTime,
    ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
        Ok(rustls::client::danger::ServerCertVerified::assertion())
    }

    fn verify_tls12_signature(
        &self,
        _message: &[u8],
        _cert: &rustls::pki_types::CertificateDer<'_>,
        _dss: &rustls::DigitallySignedStruct,
    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
        Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
    }

    fn verify_tls13_signature(
        &self,
        _message: &[u8],
        _cert: &rustls::pki_types::CertificateDer<'_>,
        _dss: &rustls::DigitallySignedStruct,
    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
        Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
    }

    fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
        vec![
            rustls::SignatureScheme::RSA_PKCS1_SHA256,
            rustls::SignatureScheme::RSA_PKCS1_SHA384,
            rustls::SignatureScheme::RSA_PKCS1_SHA512,
            rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
            rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
            rustls::SignatureScheme::ED25519,
            rustls::SignatureScheme::RSA_PSS_SHA256,
            rustls::SignatureScheme::RSA_PSS_SHA384,
            rustls::SignatureScheme::RSA_PSS_SHA512,
        ]
    }
}
