//! NATS admin integration startup — connects, initialises the log ring buffer,
//! and spawns background subscriber tasks for health-check and log-fetch
//! request-reply subjects.
//!
//! Enabled only when `nats-admin` feature is active.

use std::collections::VecDeque;
use std::sync::{Arc, Mutex};

use futures::StreamExt;
use serde_json::json;
use tracing::{error, info, warn};
use tracing_subscriber::Layer;

use crate::config::NatsAdminConnectionConfig;
use crate::nats::executor::{execute_pipeline, PipelineContext};
use crate::nats::pipeline::PipelineTrigger;
use crate::nats::{NatsAdminClient, Pipeline};

/// Maximum number of log lines kept in the ring buffer.
const LOG_RING_CAPACITY: usize = 2_000;

/// Thread-safe ring buffer for recent structured log lines.
///
/// Populated by [`NatsLogLayer`] at the tracing layer level; drained on NATS
/// admin log requests.
pub type LogBuffer = Arc<Mutex<VecDeque<String>>>;

/// A `tracing_subscriber` layer that captures formatted log lines into
/// the shared ring buffer.
///
/// Install via `tracing_subscriber::registry().with(NatsLogLayer::new(buf))`.
pub struct NatsLogLayer {
    buf: LogBuffer,
}

impl NatsLogLayer {
    pub fn new(buf: LogBuffer) -> Self {
        Self { buf }
    }
}

impl<S> Layer<S> for NatsLogLayer
where
    S: tracing::Subscriber,
{
    fn on_event(
        &self,
        event: &tracing::Event<'_>,
        _ctx: tracing_subscriber::layer::Context<'_, S>,
    ) {
        // Format: level target message (lightweight, no allocation on fast path
        // unless the ring buffer is not full yet)
        let meta = event.metadata();
        let mut visitor = MessageVisitor(String::new());
        event.record(&mut visitor);

        let line = format!("[{}] {} — {}", meta.level(), meta.target(), visitor.0,);

        if let Ok(mut buf) = self.buf.lock() {
            if buf.len() >= LOG_RING_CAPACITY {
                buf.pop_front();
            }
            buf.push_back(line);
        }
    }
}

struct MessageVisitor(String);

impl tracing::field::Visit for MessageVisitor {
    fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
        if field.name() == "message" {
            self.0 = value.to_owned();
        }
    }

    fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
        if field.name() == "message" {
            self.0 = format!("{value:?}");
        }
    }
}

/// Connect to NATS and return `(client, log_buffer)`.
///
/// Returns `Ok(None)` when `cfg.enabled` is `false` — callers should skip
/// subscriber spawning and leave the `AppState` field as `None`.
pub async fn initialize_nats_admin(
    cfg: &NatsAdminConnectionConfig,
) -> Result<Option<(NatsAdminClient, LogBuffer)>, crate::nats::client::ConnectError> {
    if !cfg.enabled {
        info!("nats-admin: disabled in config, skipping");
        return Ok(None);
    }

    let client = NatsAdminClient::connect(cfg).await?;
    let log_buffer: LogBuffer = Arc::new(Mutex::new(VecDeque::with_capacity(LOG_RING_CAPACITY)));
    Ok(Some((client, log_buffer)))
}

/// Spawn background subscriber tasks for the admin subjects.
///
/// - `admin.health` — request-reply health ping (responds with a JSON status
///   blob).
/// - `admin.logs`   — request-reply recent logs fetch (returns the last N lines
///   from the ring buffer as a JSON array).
///
/// Both tasks run indefinitely; errors are logged and the task restarts the
/// loop rather than terminating.
pub fn spawn_nats_admin_subscribers(client: NatsAdminClient, log_buffer: LogBuffer) {
    let client_health = client.clone();
    let client_logs = client.clone();
    let buf_logs = Arc::clone(&log_buffer);

    tokio::spawn(async move {
        run_health_subscriber(client_health).await;
    });

    tokio::spawn(async move {
        run_logs_subscriber(client_logs, buf_logs).await;
    });
}

async fn run_health_subscriber(client: NatsAdminClient) {
    let mut sub = match client.subscribe("health").await {
        Ok(s) => s,
        Err(e) => {
            error!(error = %e, "nats-admin: failed to subscribe to health subject");
            return;
        }
    };

    info!("nats-admin: health subscriber ready");

    while let Some(msg) = sub.next().await {
        let Some(reply) = msg.reply else {
            warn!("nats-admin: health request without reply-to, skipping");
            continue;
        };

        let hostname = std::env::var("HOSTNAME").unwrap_or_else(|_| "unknown".into());
        let payload = json!({
            "status": "ok",
            "host": hostname,
            "version": env!("CARGO_PKG_VERSION"),
        });

        let bytes = match serde_json::to_vec(&payload) {
            Ok(b) => b,
            Err(e) => {
                error!(error = %e, "nats-admin: health response serialisation failed");
                continue;
            }
        };

        if let Err(e) = client.inner().publish(reply, bytes.into()).await {
            error!(error = %e, "nats-admin: failed to publish health reply");
        }
    }

    warn!("nats-admin: health subscriber stream ended");
}

async fn run_logs_subscriber(client: NatsAdminClient, buf: LogBuffer) {
    let mut sub = match client.subscribe("logs").await {
        Ok(s) => s,
        Err(e) => {
            error!(error = %e, "nats-admin: failed to subscribe to logs subject");
            return;
        }
    };

    info!("nats-admin: logs subscriber ready");

    while let Some(msg) = sub.next().await {
        let Some(reply) = msg.reply else {
            warn!("nats-admin: logs request without reply-to, skipping");
            continue;
        };

        // Optional payload: `{"lines": N}` — defaults to 100 most recent.
        let requested: usize = serde_json::from_slice::<serde_json::Value>(&msg.payload)
            .ok()
            .and_then(|v| v.get("lines").and_then(|n| n.as_u64()))
            .unwrap_or(100) as usize;

        let lines: Vec<String> = buf
            .lock()
            .map(|guard| {
                guard
                    .iter()
                    .rev()
                    .take(requested)
                    .cloned()
                    .collect::<Vec<_>>()
                    .into_iter()
                    .rev()
                    .collect()
            })
            .unwrap_or_default();

        let payload = json!({ "lines": lines, "count": lines.len() });

        let bytes = match serde_json::to_vec(&payload) {
            Ok(b) => b,
            Err(e) => {
                error!(error = %e, "nats-admin: logs response serialisation failed");
                continue;
            }
        };

        if let Err(e) = client.inner().publish(reply, bytes.into()).await {
            error!(error = %e, "nats-admin: failed to publish logs reply");
        }
    }

    warn!("nats-admin: logs subscriber stream ended");
}

// ── Config-driven pipeline subscribers ───────────────────────────────────────

/// Spawn one background task per configured NATS pipeline.
///
/// When the client has an `EventStream` (JetStream enabled), pipelines use the
/// durable pull consumer — messages survive server restarts and require
/// explicit ack.  When JetStream is unavailable, falls back to core NATS push
/// subscriptions.
///
/// Returns the `JoinHandle` for each spawned task so callers can abort them
/// on hot-reload.  CI/CD pipelines are logged and skipped — their runtime
/// wiring lives outside the NATS subscriber loop.
pub fn spawn_pipeline_subscribers(
    client: NatsAdminClient,
    pipelines: Vec<Pipeline>,
    ctx: Arc<PipelineContext>,
) -> Vec<tokio::task::JoinHandle<()>> {
    if pipelines.is_empty() {
        info!("pipelines: none configured, skipping");
        return Vec::new();
    }

    let mut handles = Vec::new();

    for pipeline in pipelines {
        match &pipeline.trigger {
            PipelineTrigger::Nats { subject } => {
                let subject = subject.clone();
                let client = client.clone();
                let ctx = Arc::clone(&ctx);
                handles.push(tokio::spawn(async move {
                    run_nats_pipeline_subscriber(client, pipeline, subject, ctx).await;
                }));
            }
            PipelineTrigger::CiCd { event } => {
                info!(
                    pipeline = %pipeline.name,
                    event    = %event,
                    "pipelines: ci_cd pipeline registered (webhook wiring pending)"
                );
            }
        }
    }

    handles
}

/// Run a single pipeline subscriber loop.
///
/// Uses JetStream pull consumer (`EventStream`) when available — messages
/// are durable, survive restarts, and require explicit ack.
///
/// Falls back to core NATS push subscription when JetStream is disabled,
/// filtering by the configured subject pattern.
async fn run_nats_pipeline_subscriber(
    client: NatsAdminClient,
    pipeline: Pipeline,
    subject: String,
    ctx: Arc<PipelineContext>,
) {
    if let Some(event_stream) = client.event_stream() {
        run_jetstream_pipeline_subscriber(event_stream, pipeline, subject, ctx).await;
    } else {
        run_core_nats_pipeline_subscriber(client, pipeline, subject, ctx).await;
    }
}

/// JetStream pull consumer loop — durable, ack-based.
///
/// Pulls messages in batches of 10 using `pull_batch()` and executes the
/// pipeline for each message. Filter: only processes messages whose subject
/// matches the configured pipeline subject (exact or wildcard prefix).
async fn run_jetstream_pipeline_subscriber(
    event_stream: std::sync::Arc<platform_nats::EventStream>,
    pipeline: Pipeline,
    subject: String,
    ctx: Arc<PipelineContext>,
) {
    info!(
        pipeline = %pipeline.name,
        subject  = %subject,
        "pipelines: JetStream subscriber ready"
    );

    loop {
        match event_stream.pull_batch(10).await {
            Ok(messages) => {
                for (msg_subject, payload, js_msg) in messages {
                    if !subject_matches(&subject, &msg_subject) {
                        // Message belongs to a different pipeline — nack so
                        // another consumer (or future retry) can process it.
                        let _ = js_msg.ack().await;
                        continue;
                    }

                    execute_pipeline(&pipeline, &msg_subject, &payload, &ctx).await;

                    if let Err(e) = js_msg.ack().await {
                        error!(
                            pipeline = %pipeline.name,
                            subject  = %msg_subject,
                            error    = %e,
                            "pipelines: JetStream ack failed"
                        );
                    }
                }
            }
            Err(e) => {
                error!(
                    pipeline = %pipeline.name,
                    error    = %e,
                    "pipelines: JetStream pull_batch failed, retrying"
                );
                tokio::time::sleep(std::time::Duration::from_secs(2)).await;
            }
        }
    }
}

/// Core NATS push subscription fallback (when JetStream is disabled).
///
/// Uses raw subject subscription — messages are fire-and-forget (no
/// durability).
async fn run_core_nats_pipeline_subscriber(
    client: NatsAdminClient,
    pipeline: Pipeline,
    subject: String,
    ctx: Arc<PipelineContext>,
) {
    let mut sub = match client.inner().subscribe(subject.clone()).await {
        Ok(s) => s,
        Err(e) => {
            error!(
                pipeline = %pipeline.name,
                subject  = %subject,
                error    = %e,
                "pipelines: failed to subscribe to NATS subject"
            );
            return;
        }
    };

    info!(
        pipeline = %pipeline.name,
        subject  = %subject,
        "pipelines: core NATS subscriber ready (no durability)"
    );

    while let Some(msg) = sub.next().await {
        execute_pipeline(&pipeline, msg.subject.as_str(), &msg.payload, &ctx).await;
    }

    warn!(
        pipeline = %pipeline.name,
        subject  = %subject,
        "pipelines: core NATS subscriber stream ended"
    );
}

/// Check if a received subject matches a pipeline subject pattern.
///
/// Supports NATS wildcard `>` suffix: `"evol.website.dev.>"` matches any
/// subject starting with `"evol.website.dev."`.  Exact matches always pass.
fn subject_matches(pattern: &str, subject: &str) -> bool {
    if pattern == subject {
        return true;
    }
    if let Some(prefix) = pattern.strip_suffix('>') {
        return subject.starts_with(prefix);
    }
    // Single-token wildcard `*` — matches one token
    let pat_tokens: Vec<&str> = pattern.split('.').collect();
    let sub_tokens: Vec<&str> = subject.split('.').collect();
    if pat_tokens.len() != sub_tokens.len() {
        return false;
    }
    pat_tokens
        .iter()
        .zip(sub_tokens.iter())
        .all(|(p, s)| *p == "*" || p == s)
}
