use std::sync::Arc;
use std::time::Duration;

use async_nats::{ConnectOptions, HeaderMap};
use platform_nats::{EventStream, NatsConfig};
use tracing::{error, info, warn};

use super::subjects::SubjectBuilder;
use crate::config::NatsAdminConnectionConfig;

/// NATS admin client composing `platform_nats::EventStream` (JetStream + NKey
/// auth) with a raw `async_nats::Client` for request-reply operations.
///
/// # Architecture split
///
/// | Use case | Transport |
/// |---|---|
/// | Pipeline pub/consume | `EventStream` (JetStream, durable, NKey signed) |
/// | Admin health / logs | Raw client (core NATS request-reply) |
/// | Subject namespacing | `SubjectBuilder` (prefix.env.leaf) |
///
/// Cheaply cloneable: both `Arc<EventStream>` and `async_nats::Client` are
/// `Arc`-backed.
#[derive(Clone)]
pub struct NatsAdminClient {
    /// JetStream stream for pipeline event pub/consume with optional NKey auth.
    /// Wrapped in `Arc` because `EventStream` is not `Clone` (holds a pull
    /// consumer).
    event_stream: Option<Arc<EventStream>>,
    /// Raw NATS client for request-reply (health, logs) and push subscriptions.
    raw_client: async_nats::Client,
    subjects: SubjectBuilder,
    subject_map: std::collections::HashMap<String, String>,
}

impl NatsAdminClient {
    /// Connect to the NATS server.
    ///
    /// Establishes a raw connection (always) and, if `cfg.jetstream.enabled`,
    /// also connects `EventStream` which creates/ensures the configured stream
    /// and pull consumers.
    pub async fn connect(cfg: &NatsAdminConnectionConfig) -> Result<Self, ConnectError> {
        let timeout = Duration::from_secs(cfg.connection_timeout);
        let reconnect_delay = Duration::from_secs(cfg.reconnect_delay);

        let options = build_connect_options(
            &cfg.credentials_file,
            timeout,
            reconnect_delay,
            cfg.max_reconnects,
        )
        .await?;

        let raw_client = async_nats::connect_with_options(&cfg.url, options)
            .await
            .map_err(|e| ConnectError::Io(e.to_string()))?;

        info!("nats-admin: connected to {}", cfg.url);

        let event_stream = if cfg.jetstream.enabled {
            let nkey_seed = cfg
                .nkey_seed
                .as_deref()
                .filter(|s| !s.is_empty())
                .map(str::to_owned);

            let js_cfg = cfg.jetstream.pipeline_subjects(&cfg.namespace);
            let nats_cfg = NatsConfig {
                url: cfg.url.clone(),
                nkey_seed,
                stream_name: cfg.jetstream.stream_name.clone(),
                consumer_name: cfg
                    .jetstream
                    .consumers
                    .first()
                    .map(|c| c.name.clone())
                    .unwrap_or_else(|| "rustelo-pipelines".to_string()),
                subjects: js_cfg,
                require_signed_messages: cfg.require_signed_messages,
                trusted_nkeys: cfg.trusted_nkeys.clone(),
            };

            match EventStream::connect(&nats_cfg).await {
                Ok(es) => {
                    info!(
                        stream = %cfg.jetstream.stream_name,
                        "nats-admin: JetStream stream ready"
                    );
                    Some(Arc::new(es))
                }
                Err(e) => {
                    warn!(error = %e, "nats-admin: JetStream init failed, falling back to core NATS");
                    None
                }
            }
        } else {
            None
        };

        Ok(Self {
            event_stream,
            raw_client,
            subjects: SubjectBuilder::new(&cfg.namespace),
            subject_map: cfg.subjects.clone(),
        })
    }

    /// Publish a message to a named subject leaf.
    ///
    /// When JetStream is enabled, publishes via `EventStream` (NKey-signed if
    /// configured). Falls back to raw core NATS publish when JetStream is
    /// unavailable.
    pub async fn publish(
        &self,
        subject_key: &str,
        payload: impl Into<Vec<u8>>,
    ) -> Result<(), PublishError> {
        let subject = self.resolve(subject_key)?;
        let bytes = bytes::Bytes::from(payload.into());

        if let Some(es) = &self.event_stream {
            es.publish(&subject, bytes)
                .await
                .map_err(|e| PublishError::EventStream(e.to_string()))
        } else {
            self.raw_client
                .publish(subject, bytes)
                .await
                .map_err(PublishError::Nats)
        }
    }

    /// Publish with explicit NATS headers (e.g. for structured metadata).
    ///
    /// Always uses the raw client — header customisation is not available
    /// through `EventStream` (which manages its own NKey headers).
    pub async fn publish_with_headers(
        &self,
        subject_key: &str,
        headers: HeaderMap,
        payload: impl Into<Vec<u8>>,
    ) -> Result<(), PublishError> {
        let subject = self.resolve(subject_key)?;
        self.raw_client
            .publish_with_headers(subject, headers, payload.into().into())
            .await
            .map_err(PublishError::Nats)
    }

    /// Send a request and await a single reply (request-reply pattern).
    ///
    /// Used for admin health checks and log streaming.  Always uses the raw
    /// client — `platform_nats::EventStream` does not support request-reply.
    pub async fn request(
        &self,
        subject_key: &str,
        payload: impl Into<Vec<u8>>,
    ) -> Result<async_nats::Message, RequestError> {
        let subject = self.resolve(subject_key)?;
        self.raw_client
            .request(subject, payload.into().into())
            .await
            .map_err(RequestError::Nats)
    }

    /// Subscribe to a named subject leaf for incoming admin commands.
    ///
    /// Returns a push-based `async_nats::Subscriber`. Used by admin health
    /// and log subscribers which need request-reply semantics.
    pub async fn subscribe(
        &self,
        subject_key: &str,
    ) -> Result<async_nats::Subscriber, SubscribeError> {
        let subject = self.resolve(subject_key)?;
        self.raw_client
            .subscribe(subject)
            .await
            .map_err(SubscribeError::Nats)
    }

    /// Raw `async_nats::Client` for advanced operations (direct pub, req-reply
    /// to non-mapped subjects, JetStream via `jetstream::new(client)`).
    pub fn inner(&self) -> &async_nats::Client {
        &self.raw_client
    }

    /// Access the JetStream `EventStream` when enabled.
    ///
    /// Pipeline subscribers use this to pull from the durable consumer.
    pub fn event_stream(&self) -> Option<Arc<EventStream>> {
        self.event_stream.clone()
    }

    pub fn subjects(&self) -> &SubjectBuilder {
        &self.subjects
    }

    fn resolve(&self, key: &str) -> Result<String, UnknownSubject> {
        self.subjects
            .build_from_map(&self.subject_map, key)
            .ok_or_else(|| UnknownSubject(key.to_string()))
    }
}

async fn build_connect_options(
    credentials_file: &str,
    timeout: Duration,
    reconnect_delay: Duration,
    max_reconnects: usize,
) -> Result<ConnectOptions, ConnectError> {
    let base = if credentials_file.is_empty() || credentials_file.starts_with("${") {
        warn!("nats-admin: no credentials_file configured, connecting without auth");
        ConnectOptions::new()
    } else {
        ConnectOptions::with_credentials_file(std::path::PathBuf::from(credentials_file))
            .await
            .map_err(|e| {
                error!("nats-admin: credentials load failed from '{credentials_file}': {e}");
                ConnectError::Credentials(e.to_string())
            })?
    };

    Ok(base
        .connection_timeout(timeout)
        .max_reconnects(max_reconnects)
        .reconnect_delay_callback(move |_| reconnect_delay)
        .event_callback(|event| async move {
            match event {
                async_nats::Event::Connected => info!("nats-admin: connected"),
                async_nats::Event::Disconnected => warn!("nats-admin: disconnected"),
                async_nats::Event::Draining => warn!("nats-admin: draining"),
                async_nats::Event::Closed => warn!("nats-admin: connection closed"),
                async_nats::Event::LameDuckMode => warn!("nats-admin: server lame-duck mode"),
                async_nats::Event::SlowConsumer(n) => {
                    warn!("nats-admin: slow consumer ({n} dropped)")
                }
                async_nats::Event::ServerError(e) => error!("nats-admin: server error: {e}"),
                async_nats::Event::ClientError(e) => error!("nats-admin: client error: {e}"),
            }
        }))
}

#[derive(Debug)]
struct UnknownSubject(String);

#[derive(Debug, thiserror::Error)]
pub enum ConnectError {
    #[error("failed to load NATS credentials: {0}")]
    Credentials(String),
    #[error("NATS connection error: {0}")]
    Io(String),
}

#[derive(Debug, thiserror::Error)]
pub enum PublishError {
    #[error("unknown subject key '{0}' — not in nats_admin.subjects config")]
    UnknownSubject(String),
    #[error("NATS publish error: {0}")]
    Nats(#[from] async_nats::PublishError),
    #[error("JetStream publish error: {0}")]
    EventStream(String),
}

impl From<UnknownSubject> for PublishError {
    fn from(e: UnknownSubject) -> Self {
        Self::UnknownSubject(e.0)
    }
}

#[derive(Debug, thiserror::Error)]
pub enum RequestError {
    #[error("unknown subject key '{0}' — not in nats_admin.subjects config")]
    UnknownSubject(String),
    #[error("NATS request error: {0}")]
    Nats(#[from] async_nats::RequestError),
}

impl From<UnknownSubject> for RequestError {
    fn from(e: UnknownSubject) -> Self {
        Self::UnknownSubject(e.0)
    }
}

#[derive(Debug, thiserror::Error)]
pub enum SubscribeError {
    #[error("unknown subject key '{0}' — not in nats_admin.subjects config")]
    UnknownSubject(String),
    #[error("NATS subscribe error: {0}")]
    Nats(#[from] async_nats::SubscribeError),
}

impl From<UnknownSubject> for SubscribeError {
    fn from(e: UnknownSubject) -> Self {
        Self::UnknownSubject(e.0)
    }
}
