//! Audit log view — typed rendering per [`AuditEvent`] variant.
//!
//! Replaces the v0.2.0 raw-JSON-line dump. Each entry is shown as a
//! single row with:
//! - an event-type icon
//! - a human-readable summary (binary name, secret name, signal id,
//!   …) extracted from the typed event payload
//! - a relative timestamp ("2m ago") computed against
//!   `SystemTime::now`
//! - a faint chain-hash chip the operator can hover for forensic
//!   correlation
//!
//! All structured rendering — never `format!("{event:?}")`. The
//! [`envseal::audit::ParsedEntry`] type is the single source of
//! truth for the JSON shape; this file is purely presentation.

use eframe::egui;
use envseal::audit::{AuditEvent, ParsedEntry};

use crate::worker::ReqId;

#[derive(Default)]
pub struct AuditState {
    pub entries: Vec<ParsedEntry>,
    pub dropped_lines: usize,
    pending: Option<ReqId>,
    /// Free-text filter — case-insensitive substring match against
    /// the human-readable event summary.
    pub filter_query: String,
    /// Event-type dropdown selection. Empty = show all.
    pub filter_event_type: String,
}

/// Known event type tags for the dropdown filter.
const EVENT_TYPES: &[(&str, &str)] = &[
    ("", "All events"),
    ("secret_stored", "Secret stored"),
    ("secret_revoked", "Secret revoked"),
    ("secret_accessed", "Secret accessed"),
    ("approval_requested", "Approval requested"),
    ("approval_result", "Approval result"),
    ("signal_recorded", "Signal recorded"),
    ("tier_changed", "Tier changed"),
    ("rate_limited", "Rate limited"),
    ("challenge_attempted", "Challenge attempted"),
    ("supervisor_leak_detected", "Leak detected"),
    ("secret_store_failed", "Store failed"),
];

impl AuditState {
    pub fn start_refresh(&mut self, id: ReqId) {
        self.pending = Some(id);
    }

    pub fn apply_loaded(&mut self, id: ReqId, entries: Vec<ParsedEntry>, dropped_lines: usize) {
        if self.pending != Some(id) {
            return;
        }
        self.pending = None;
        self.entries = entries;
        self.dropped_lines = dropped_lines;
    }

    fn is_loading(&self) -> bool {
        self.pending.is_some()
    }

    /// Build an [`envseal::audit::AuditFilter`] from the current UI state.
    pub fn as_filter(&self) -> envseal::audit::AuditFilter {
        envseal::audit::AuditFilter {
            query: if self.filter_query.is_empty() {
                None
            } else {
                Some(self.filter_query.clone())
            },
            event_type: if self.filter_event_type.is_empty() {
                None
            } else {
                Some(self.filter_event_type.clone())
            },
        }
    }

    /// Test whether a parsed entry passes the current local filter.
    fn matches_entry(&self, entry: &ParsedEntry) -> bool {
        // Event type filter
        if !self.filter_event_type.is_empty() {
            let tag = event_tag(&entry.event);
            if tag != self.filter_event_type {
                return false;
            }
        }
        // Free-text filter (match against the human summary)
        if !self.filter_query.is_empty() {
            let q = self.filter_query.to_ascii_lowercase();
            let summary = event_summary(&entry.event).to_ascii_lowercase();
            let ts = entry.ts.to_ascii_lowercase();
            if !summary.contains(&q) && !ts.contains(&q) {
                return false;
            }
        }
        true
    }
}

/// Returns true when the user clicked Refresh.
pub fn render(ui: &mut egui::Ui, state: &mut AuditState) -> bool {
    let mut want_refresh = false;

    ui.add_space(10.0);
    ui.horizontal(|ui| {
        ui.heading("Audit Log");
        if state.is_loading() {
            ui.add_space(8.0);
            ui.spinner();
        }
        ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
            if ui.button("Refresh").clicked() {
                want_refresh = true;
            }
        });
    });
    ui.label(
        egui::RichText::new("Hash-chained, append-only. Newest first.")
            .size(12.0)
            .weak(),
    );

    // ── Filter bar ───────────────────────────────────────────────
    ui.add_space(6.0);
    egui::Frame::new()
        .fill(ui.visuals().faint_bg_color)
        .corner_radius(6.0)
        .inner_margin(egui::Margin::symmetric(10, 6))
        .show(ui, |ui| {
            ui.horizontal(|ui| {
                ui.label(egui::RichText::new("🔍").size(14.0));
                ui.add_space(4.0);
                let search = egui::TextEdit::singleline(&mut state.filter_query)
                    .hint_text("Search audit entries…")
                    .desired_width(200.0);
                if ui.add(search).changed() {
                    want_refresh = true;
                }

                ui.add_space(12.0);
                ui.label(egui::RichText::new("Event type:").size(12.0).weak());
                let current_label = EVENT_TYPES
                    .iter()
                    .find(|(tag, _)| *tag == state.filter_event_type)
                    .map_or("All events", |(_, label)| label);
                let mut combobox_changed = false;
                egui::ComboBox::from_id_salt("audit_event_filter")
                    .selected_text(current_label)
                    .show_ui(ui, |ui| {
                        for (tag, label) in EVENT_TYPES {
                            if ui
                                .selectable_value(
                                    &mut state.filter_event_type,
                                    (*tag).to_string(),
                                    *label,
                                )
                                .clicked()
                            {
                                combobox_changed = true;
                            }
                        }
                    });
                if combobox_changed {
                    want_refresh = true;
                }

                if !state.filter_query.is_empty() || !state.filter_event_type.is_empty() {
                    ui.add_space(8.0);
                    if ui
                        .small_button("✕ Clear")
                        .on_hover_text("Clear all filters")
                        .clicked()
                    {
                        state.filter_query.clear();
                        state.filter_event_type.clear();
                        want_refresh = true;
                    }
                }
            });
        });

    if state.dropped_lines > 0 {
        ui.add_space(6.0);
        ui.colored_label(
            egui::Color32::from_rgb(0xf5, 0x9e, 0x0b),
            format!(
                "⚠ {} log line(s) failed to parse — chain rotation may have happened",
                state.dropped_lines
            ),
        );
    }
    ui.add_space(6.0);

    // Collect filtered entries
    let filtered: Vec<&ParsedEntry> = state
        .entries
        .iter()
        .filter(|e| state.matches_entry(e))
        .collect();

    if filtered.is_empty() && !state.is_loading() {
        ui.add_space(40.0);
        ui.vertical_centered(|ui| {
            if state.entries.is_empty() {
                ui.label(
                    egui::RichText::new("No audit entries yet.")
                        .size(14.0)
                        .weak(),
                );
                ui.add_space(4.0);
                ui.label(
                    egui::RichText::new(
                        "Entries are written when secrets are stored, revoked, or accessed.",
                    )
                    .size(12.0)
                    .weak(),
                );
            } else {
                ui.label(
                    egui::RichText::new("No entries match the current filter.")
                        .size(14.0)
                        .weak(),
                );
            }
        });
        return want_refresh;
    }

    // Show match count when filtering
    if !state.filter_query.is_empty() || !state.filter_event_type.is_empty() {
        ui.label(
            egui::RichText::new(format!(
                "Showing {} of {} entries",
                filtered.len(),
                state.entries.len()
            ))
            .size(11.0)
            .weak(),
        );
        ui.add_space(4.0);
    }

    let now = std::time::SystemTime::now();
    egui::ScrollArea::vertical().show(ui, |ui| {
        for entry in &filtered {
            render_row(ui, entry, now);
            ui.add_space(2.0);
        }
    });

    want_refresh
}

fn render_row(ui: &mut egui::Ui, entry: &ParsedEntry, now: std::time::SystemTime) {
    let (icon, color) = event_glyph(&entry.event);
    let summary = event_summary(&entry.event);
    let relative_ts = relative_time(&entry.ts, now);

    egui::Frame::new()
        .fill(ui.visuals().faint_bg_color)
        .corner_radius(6.0)
        .inner_margin(egui::Margin::symmetric(10, 8))
        .show(ui, |ui| {
            ui.horizontal(|ui| {
                ui.label(egui::RichText::new(icon).size(18.0).color(color));
                ui.add_space(6.0);
                ui.vertical(|ui| {
                    ui.label(egui::RichText::new(&summary).size(13.0));
                    ui.horizontal(|ui| {
                        ui.label(
                            egui::RichText::new(&relative_ts)
                                .size(10.5)
                                .weak()
                                .monospace(),
                        );
                        ui.add_space(8.0);
                        ui.label(
                            egui::RichText::new(format!("pid {}", entry.pid))
                                .size(10.5)
                                .weak()
                                .monospace(),
                        );
                        ui.add_space(8.0);
                        let chain_short: String = entry.chain.chars().take(8).collect();
                        ui.label(
                            egui::RichText::new(format!("chain {chain_short}…"))
                                .size(10.5)
                                .weak()
                                .monospace(),
                        )
                        .on_hover_text(&entry.chain);
                    });
                });
            });
        });
}

/// Map each event variant to its JSON `event` tag string — the same
/// value that appears in the serialized log. Used by the local filter
/// to match entries by event type without re-serializing.
fn event_tag(event: &AuditEvent) -> &'static str {
    match event {
        AuditEvent::ApprovalRequested { .. } => "approval_requested",
        AuditEvent::ApprovalResult { .. } => "approval_result",
        AuditEvent::SecretStored { .. } => "secret_stored",
        AuditEvent::SecretRevoked { .. } => "secret_revoked",
        AuditEvent::SignalRecorded { .. } => "signal_recorded",
        AuditEvent::ChallengeAttempted { .. } => "challenge_attempted",
        AuditEvent::TierChanged { .. } => "tier_changed",
        AuditEvent::SupervisorLeakDetected { .. } => "supervisor_leak_detected",
        AuditEvent::RateLimited { .. } => "rate_limited",
        AuditEvent::SecretAccessed { .. } => "secret_accessed",
        AuditEvent::SecretStoreFailed { .. } => "secret_store_failed",
    }
}

/// Map each event variant to a glyph + color so the operator can
/// scan the log visually without reading every line.
fn event_glyph(event: &AuditEvent) -> (&'static str, egui::Color32) {
    let green = egui::Color32::from_rgb(0x22, 0xc5, 0x5e);
    let amber = egui::Color32::from_rgb(0xf5, 0x9e, 0x0b);
    let red = egui::Color32::from_rgb(0xef, 0x44, 0x44);
    let blue = egui::Color32::from_rgb(0x3b, 0x82, 0xf6);
    match event {
        AuditEvent::ApprovalRequested { .. } => ("●", blue),
        AuditEvent::ApprovalResult { decision, .. } => {
            if decision == "Deny" {
                ("✕", red)
            } else {
                ("✓", green)
            }
        }
        AuditEvent::SecretStored { .. } => ("＋", green),
        AuditEvent::SecretRevoked { .. } => ("－", amber),
        AuditEvent::SignalRecorded { .. } => ("▲", amber),
        AuditEvent::ChallengeAttempted { passed: true } => ("⌨", green),
        AuditEvent::ChallengeAttempted { passed: false } => ("⌨", red),
        AuditEvent::TierChanged { .. } => ("⇧", blue),
        AuditEvent::SupervisorLeakDetected { .. } => ("⚠", red),
        AuditEvent::RateLimited { .. } => ("⏳", amber),
        AuditEvent::SecretAccessed { .. } => ("→", blue),
        AuditEvent::SecretStoreFailed { .. } => ("✕", red),
    }
}

/// One-line human summary per event variant — typed extraction
/// from the [`AuditEvent`] payload, never `Debug`-formatting.
fn event_summary(event: &AuditEvent) -> String {
    match event {
        AuditEvent::ApprovalRequested { binary, secret } => {
            format!("approval requested: '{binary}' wants '{secret}'")
        }
        AuditEvent::ApprovalResult {
            binary,
            secret,
            decision,
        } => format!("approval {decision}: '{binary}' for '{secret}'"),
        AuditEvent::SecretStored { name } => format!("stored '{name}'"),
        AuditEvent::SecretRevoked { name } => format!("revoked '{name}'"),
        AuditEvent::SignalRecorded {
            tier,
            classification,
        } => format!("signal @ {tier}: {classification}"),
        AuditEvent::ChallengeAttempted { passed: true } => "challenge passed".to_string(),
        AuditEvent::ChallengeAttempted { passed: false } => "challenge FAILED".to_string(),
        AuditEvent::TierChanged { from, to } => format!("tier changed: {from} → {to}"),
        AuditEvent::SupervisorLeakDetected {
            secret,
            binary,
            leak_count,
        } => format!("LEAK: '{binary}' leaked '{secret}' {leak_count} time(s) — REDACTED"),
        AuditEvent::RateLimited { reason } => format!("rate-limited: {reason}"),
        AuditEvent::SecretAccessed { binary, secret } => {
            format!("'{binary}' accessed '{secret}'")
        }
        AuditEvent::SecretStoreFailed { name, reason } => {
            format!("store FAILED for '{name}': {reason}")
        }
    }
}

/// Format an ISO-8601 audit timestamp as "Nm ago" / "Nh ago" /
/// "Nd ago". Falls back to the absolute string on parse failure
/// rather than dropping the row.
fn relative_time(ts: &str, now: std::time::SystemTime) -> String {
    let Some(then) = parse_iso8601_to_unix(ts) else {
        return ts.to_string();
    };
    let now_unix = now
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_secs())
        .unwrap_or(0) as i64;
    let elapsed = now_unix - then;
    if elapsed < 0 {
        return ts.to_string(); // clock skew — show absolute
    }
    if elapsed < 60 {
        return format!("{elapsed}s ago");
    }
    if elapsed < 3600 {
        return format!("{}m ago", elapsed / 60);
    }
    if elapsed < 86_400 {
        return format!("{}h ago", elapsed / 3600);
    }
    let days = elapsed / 86_400;
    if days < 30 {
        format!("{days}d ago")
    } else {
        ts.to_string() // older than a month — show full date
    }
}

/// Parse `2026-05-03T02:23:44Z` → unix seconds. Hand-rolled to
/// avoid pulling `chrono` / `time` into the GUI binary; the format
/// envseal writes is fixed and narrow enough that fixed-position
/// parsing is robust.
fn parse_iso8601_to_unix(ts: &str) -> Option<i64> {
    if ts.len() < 20 || !ts.ends_with('Z') {
        return None;
    }
    let bytes = ts.as_bytes();
    if bytes[4] != b'-'
        || bytes[7] != b'-'
        || bytes[10] != b'T'
        || bytes[13] != b':'
        || bytes[16] != b':'
    {
        return None;
    }
    let y: i64 = ts[0..4].parse().ok()?;
    let mo: i64 = ts[5..7].parse().ok()?;
    let d: i64 = ts[8..10].parse().ok()?;
    let h: i64 = ts[11..13].parse().ok()?;
    let mi: i64 = ts[14..16].parse().ok()?;
    let s: i64 = ts[17..19].parse().ok()?;
    Some(days_from_civil(y, mo, d) * 86_400 + h * 3600 + mi * 60 + s)
}

/// Howard Hinnant's days-from-civil — converts a (y, m, d) date to
/// days since the Unix epoch. Constant-time, no leap-year branches
/// in the hot path.
fn days_from_civil(y: i64, m: i64, d: i64) -> i64 {
    let y = if m <= 2 { y - 1 } else { y };
    let era = if y >= 0 { y } else { y - 399 } / 400;
    let yoe = y - era * 400;
    let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d - 1;
    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
    era * 146_097 + doe - 719_468
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_iso8601_unix_epoch() {
        // 1970-01-01T00:00:00Z = 0
        assert_eq!(parse_iso8601_to_unix("1970-01-01T00:00:00Z"), Some(0));
    }

    #[test]
    fn parse_iso8601_known_date() {
        // 2026-05-03T02:23:44Z. Day count from 1970-01-01:
        // 56 years × 365 + 14 leap-day inserts = 20,454 days to
        // 2026-01-01; +122 days to reach 2026-05-03 (Jan 31 + Feb
        // 28 + Mar 31 + Apr 30 + 2) = 20,576 days.
        // 20576 * 86400 + 2*3600 + 23*60 + 44 = 1_777_775_024.
        assert_eq!(
            parse_iso8601_to_unix("2026-05-03T02:23:44Z"),
            Some(1_777_775_024)
        );
    }

    #[test]
    fn parse_iso8601_rejects_malformed() {
        assert!(parse_iso8601_to_unix("not a timestamp").is_none());
        assert!(parse_iso8601_to_unix("2026-05-03 02:23:44Z").is_none()); // missing T
        assert!(parse_iso8601_to_unix("2026-05-03T02:23:44").is_none()); // missing Z
    }

    #[test]
    fn relative_time_seconds() {
        // The reference timestamp parses to 1_777_775_024. Use a
        // 59-second offset so we land squarely in the `< 60` branch
        // (60s exactly rolls over to "1m ago", which is also
        // correct but tests the wrong branch).
        let now = std::time::UNIX_EPOCH + std::time::Duration::from_secs(1_777_775_083);
        assert_eq!(relative_time("2026-05-03T02:23:44Z", now), "59s ago");
    }

    #[test]
    fn relative_time_minutes_at_boundary() {
        // 60s exactly should report "1m ago".
        let now = std::time::UNIX_EPOCH + std::time::Duration::from_secs(1_777_775_084);
        assert_eq!(relative_time("2026-05-03T02:23:44Z", now), "1m ago");
    }
}
