//! Tracks event transport + builder.

use std::sync::Mutex;
use std::thread::{self, JoinHandle};
use std::time::Duration;

use chrono::{DateTime, Utc};
use reqwest::blocking::Client;

pub use fluidattacks_tracks_domain::event::{Action, Event, Mechanism};

/// Builder for [`Event`]; `date` defaults to [`Utc::now`], `metadata` to `{}`.
pub struct EventBuilder {
    event: Event,
}

impl EventBuilder {
    #[must_use]
    pub fn new(
        action: Action,
        author: String,
        mechanism: Mechanism,
        object: String,
        object_id: String,
    ) -> Self {
        Self {
            event: Event::new(
                action,
                author,
                Utc::now(),
                mechanism,
                serde_json::Value::Object(serde_json::Map::new()),
                object,
                object_id,
            ),
        }
    }

    #[must_use]
    pub const fn date(mut self, date: DateTime<Utc>) -> Self {
        self.event.date = date;
        self
    }
    #[must_use]
    pub fn metadata(mut self, metadata: serde_json::Value) -> Self {
        self.event.metadata = metadata;
        self
    }

    #[must_use]
    pub const fn author_anonymous(mut self, value: bool) -> Self {
        self.event.author_anonymous = Some(value);
        self
    }

    #[must_use]
    pub fn author_ip(mut self, value: String) -> Self {
        self.event.author_ip = Some(value);
        self
    }

    #[must_use]
    pub fn author_role(mut self, value: String) -> Self {
        self.event.author_role = Some(value);
        self
    }

    #[must_use]
    pub fn author_user_agent(mut self, value: String) -> Self {
        self.event.author_user_agent = Some(value);
        self
    }

    #[must_use]
    pub fn session_id(mut self, value: String) -> Self {
        self.event.session_id = Some(value);
        self
    }

    #[must_use]
    pub fn build(self) -> Event {
        self.event
    }
}

fn send_with_retry(http: &Client, url: &str, event: &Event, retry_attempts: u32) {
    for attempt in 0..retry_attempts {
        let ok = http
            .post(url)
            .json(event)
            .send()
            .is_ok_and(|r| r.status().is_success());
        if ok {
            return;
        }
        if attempt.saturating_add(1) < retry_attempts {
            let delay = 100u64.saturating_mul(2u64.saturating_pow(attempt));
            thread::sleep(Duration::from_millis(delay));
        }
    }
}

/// Tracks event resource.
pub struct EventResource {
    base_url: String,
    http: Client,
    retry_attempts: u32,
    pending: Mutex<Vec<JoinHandle<()>>>,
}

impl EventResource {
    pub(crate) const fn new(base_url: String, http: Client, retry_attempts: u32) -> Self {
        Self {
            base_url,
            http,
            retry_attempts,
            pending: Mutex::new(Vec::new()),
        }
    }

    /// Publish an event fire-and-forget in a background thread.
    ///
    /// Errors are silently swallowed so that tracking failures never
    /// interrupt the caller's normal flow. Call [`EventResource::flush`]
    /// before process exit to ensure all in-flight events are delivered.
    ///
    /// # Panics
    ///
    /// Does not panic. If `event.metadata` is not a JSON object the event is
    /// silently dropped.
    pub fn create(&self, event: Event) {
        if !event.metadata.is_object() {
            return;
        }
        let url = format!("{}/event", self.base_url.trim_end_matches('/'));
        let http = self.http.clone();
        let retry_attempts = self.retry_attempts;
        let handle = thread::spawn(move || {
            send_with_retry(&http, &url, &event, retry_attempts);
        });
        if let Ok(mut pending) = self.pending.lock() {
            pending.retain(|h| !h.is_finished());
            pending.push(handle);
        }
    }

    /// Block until all in-flight events have been sent or exhausted their retries.
    ///
    /// Call this before process exit to avoid dropping events that are still
    /// being delivered in background threads.
    pub fn flush(&self) {
        let handles = self
            .pending
            .lock()
            .map(|mut p| std::mem::take(&mut *p))
            .unwrap_or_default();
        for handle in handles {
            let _ = handle.join();
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{Action, EventBuilder, Mechanism};

    #[test]
    fn builder_sets_fields_and_defaults() {
        let event = EventBuilder::new(
            Action::Create,
            "alice".to_owned(),
            Mechanism::Api,
            "user".to_owned(),
            "u1".to_owned(),
        )
        .session_id("s1".to_owned())
        .build();

        assert_eq!(event.author.as_str(), "alice");
        assert_eq!(event.object.as_str(), "user");
        assert_eq!(event.object_id.as_str(), "u1");
        assert_eq!(event.session_id.as_deref(), Some("s1"));
        assert_eq!(event.metadata, serde_json::json!({}));
        assert_eq!(event.author_anonymous, None);
    }
}
