use std::time::Duration;

use anyhow::Result;
use chainfire_client::{Client, EventType};
use tracing::{info, warn};

#[allow(dead_code)]
#[derive(Debug, Clone, Copy)]
pub enum WatchScope {
    Key,
    Prefix,
}

#[derive(Debug, Clone)]
pub struct WatchChange {
    pub event_type: EventType,
    pub key: Vec<u8>,
    pub value: Vec<u8>,
    #[allow(dead_code)]
    pub revision: u64,
}

pub struct ChainfireWatcher {
    endpoint: String,
    scope: WatchScope,
    target: Vec<u8>,
    reconnect_backoff: Duration,
}

impl ChainfireWatcher {
    #[allow(dead_code)]
    pub fn key(endpoint: String, key: Vec<u8>) -> Self {
        Self {
            endpoint,
            scope: WatchScope::Key,
            target: key,
            reconnect_backoff: Duration::from_secs(1),
        }
    }

    pub fn prefix(endpoint: String, prefix: Vec<u8>) -> Self {
        Self {
            endpoint,
            scope: WatchScope::Prefix,
            target: prefix,
            reconnect_backoff: Duration::from_secs(1),
        }
    }

    #[allow(dead_code)]
    pub async fn watch<F>(&self, mut callback: F) -> Result<()>
    where
        F: FnMut(WatchChange) -> Result<()>,
    {
        self.watch_with_ready(|| Ok(()), move |change| callback(change))
            .await
    }

    pub async fn watch_with_ready<F, G>(&self, mut on_connected: G, mut callback: F) -> Result<()>
    where
        F: FnMut(WatchChange) -> Result<()>,
        G: FnMut() -> Result<()>,
    {
        loop {
            match Client::connect(self.endpoint.clone()).await {
                Ok(mut client) => {
                    let watch_result = match self.scope {
                        WatchScope::Key => client.watch(&self.target).await,
                        WatchScope::Prefix => client.watch_prefix(&self.target).await,
                    };

                    match watch_result {
                        Ok(mut handle) => {
                            info!(
                                scope = ?self.scope,
                                target = %String::from_utf8_lossy(&self.target),
                                watch_id = handle.id(),
                                "connected ChainFire watch"
                            );
                            on_connected()?;

                            while let Some(event) = handle.recv().await {
                                if let Err(error) = callback(WatchChange {
                                    event_type: event.event_type,
                                    key: event.key,
                                    value: event.value,
                                    revision: event.revision,
                                }) {
                                    warn!(error = %error, "watch callback failed");
                                }
                            }

                            warn!(
                                scope = ?self.scope,
                                target = %String::from_utf8_lossy(&self.target),
                                "ChainFire watch stream ended; reconnecting"
                            );
                        }
                        Err(error) => {
                            warn!(
                                error = %error,
                                scope = ?self.scope,
                                target = %String::from_utf8_lossy(&self.target),
                                "failed to create ChainFire watch"
                            );
                        }
                    }
                }
                Err(error) => {
                    warn!(
                        error = %error,
                        scope = ?self.scope,
                        target = %String::from_utf8_lossy(&self.target),
                        "failed to connect ChainFire watch"
                    );
                }
            }

            tokio::time::sleep(self.reconnect_backoff).await;
        }
    }
}
