//! In-memory client state registry — the "who has contacted us" table the
//! web UI displays. Not persisted: PXE sessions are ephemeral by nature.

use crate::arch::ClientArch;
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::net::IpAddr;
use std::sync::Arc;
use time::OffsetDateTime;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ClientEvent {
    DhcpDiscover,
    DhcpRequest,
    PxeBootServerRequest,
    TftpRead { file: String },
    HttpScriptFetch { target: String },
    HttpIsoAsset { file: String },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClientSnapshot {
    pub mac: String,
    pub last_ip: Option<IpAddr>,
    pub arch: Option<ClientArch>,
    pub hostname: Option<String>,
    #[serde(with = "time::serde::rfc3339")]
    pub first_seen: OffsetDateTime,
    #[serde(with = "time::serde::rfc3339")]
    pub last_seen: OffsetDateTime,
    // Events are left with default serialization (9-tuple) — they're
    // diagnostic only and not consumed by the UI today.
    pub events: Vec<(OffsetDateTime, ClientEvent)>,
    /// The boot target (ISO id) last selected via the iPXE menu, if any.
    pub selected_target: Option<String>,
}

#[derive(Debug, Default)]
pub struct ClientRegistry {
    inner: RwLock<HashMap<String, ClientSnapshot>>,
}

impl ClientRegistry {
    #[must_use]
    pub fn new() -> Arc<Self> {
        Arc::new(Self::default())
    }

    pub fn record(
        &self,
        mac: &str,
        ip: Option<IpAddr>,
        arch: Option<ClientArch>,
        event: ClientEvent,
    ) {
        let mut guard = self.inner.write();
        let now = OffsetDateTime::now_utc();
        let entry = guard
            .entry(mac.to_string())
            .or_insert_with(|| ClientSnapshot {
                mac: mac.to_string(),
                last_ip: ip,
                arch,
                hostname: None,
                first_seen: now,
                last_seen: now,
                events: Vec::new(),
                selected_target: None,
            });
        entry.last_seen = now;
        if ip.is_some() {
            entry.last_ip = ip;
        }
        if arch.is_some() {
            entry.arch = arch;
        }
        entry.events.push((now, event));
        // Cap event history per client to keep memory bounded.
        const MAX_EVENTS: usize = 64;
        if entry.events.len() > MAX_EVENTS {
            let drop_n = entry.events.len() - MAX_EVENTS;
            entry.events.drain(..drop_n);
        }
    }

    pub fn set_selected_target(&self, mac: &str, target: Option<String>) {
        let mut guard = self.inner.write();
        if let Some(c) = guard.get_mut(mac) {
            c.selected_target = target;
        }
    }

    #[must_use]
    pub fn list(&self) -> Vec<ClientSnapshot> {
        let guard = self.inner.read();
        let mut v: Vec<_> = guard.values().cloned().collect();
        // Reverse-chronological by last-seen (most recent first).
        v.sort_by_key(|c| std::cmp::Reverse(c.last_seen));
        v
    }

    #[must_use]
    pub fn get(&self, mac: &str) -> Option<ClientSnapshot> {
        self.inner.read().get(mac).cloned()
    }
}
