use serde::Serialize;
use std::collections::BTreeMap;

const MAX_RESTART_CHURN: usize = 8;

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct RestartChurnSummary {
    pub family: String,
    pub occurrences: usize,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub max_restart_count: Option<usize>,
    pub sample: String,
}

#[derive(Debug, Default)]
pub struct RestartChurnState {
    counts: BTreeMap<String, usize>,
    max_restart_counts: BTreeMap<String, usize>,
    samples: BTreeMap<String, String>,
}

impl RestartChurnState {
    pub fn observe(&mut self, event_name: &str, detail: &str) {
        let restart_count = extract_field(detail, "restart_count").and_then(parse_usize);
        let sample = truncate_detail(detail, 140);
        for family in classify_restart_churn_families(event_name, detail) {
            *self.counts.entry(family.to_string()).or_default() += 1;
            if let Some(count) = restart_count {
                let entry = self
                    .max_restart_counts
                    .entry(family.to_string())
                    .or_default();
                *entry = (*entry).max(count);
            }
            self.samples
                .entry(family.to_string())
                .or_insert_with(|| sample.clone());
        }
    }

    pub fn groups(&self) -> usize {
        self.counts.len()
    }

    pub fn summaries(&self) -> Vec<RestartChurnSummary> {
        let mut summaries = self
            .counts
            .iter()
            .map(|(family, occurrences)| RestartChurnSummary {
                family: family.clone(),
                occurrences: *occurrences,
                max_restart_count: self.max_restart_counts.get(family).copied(),
                sample: self.samples.get(family).cloned().unwrap_or_default(),
            })
            .collect::<Vec<_>>();
        summaries.sort_by(|left, right| {
            right
                .occurrences
                .cmp(&left.occurrences)
                .then(left.family.cmp(&right.family))
        });
        summaries.truncate(MAX_RESTART_CHURN);
        summaries
    }
}

pub fn classify_restart_churn_families(event_name: &str, detail: &str) -> Vec<&'static str> {
    let mut families = Vec::new();

    if is_fresh_restart(event_name, detail) {
        families.push("fresh_restart");
    }
    if event_name == "auto_trigger_timeout" {
        families.push("auto_trigger_timeout");
    }
    if is_ctrl_d_restart_loop(event_name) {
        families.push("ctrl_d_restart_loop");
    }
    if is_quit_after_eof(event_name, detail) {
        families.push("quit_after_eof");
    }

    families
}

pub fn is_restart_churn_warning_family(family: &str) -> bool {
    family != "quit_after_eof"
}

fn is_fresh_restart(event_name: &str, detail: &str) -> bool {
    if matches!(
        event_name,
        "claude_start" | "codex_start" | "claude_restart" | "codex_restart"
    ) {
        return extract_field(detail, "mode") == Some("fresh_restart");
    }

    if event_name == "ipc_restart" {
        return extract_field(detail, "mode") == Some("fresh");
    }

    matches!(
        event_name,
        "fresh_restart_before_prompt"
            | "ctrl_d_restart_fresh"
            | "ctrl_d_before_prompt_restart_fresh"
            | "ctrl_d_committed_cycle_restart_fresh"
    )
}

fn is_ctrl_d_restart_loop(event_name: &str) -> bool {
    matches!(
        event_name,
        "ctrl_d_restart_fresh"
            | "ctrl_d_before_prompt_restart_fresh"
            | "ctrl_d_committed_cycle_restart_fresh"
    )
}

fn is_quit_after_eof(event_name: &str, detail: &str) -> bool {
    if matches!(event_name, "user_quit_after_eof" | "user_quit_after_ctrl_d") {
        return true;
    }

    event_name == "supervisor_exit"
        && extract_field(detail, "reason")
            .is_some_and(|reason| reason.starts_with("user_quit_after_"))
}

fn extract_field<'a>(detail: &'a str, key: &str) -> Option<&'a str> {
    let needle = format!("{key}=");
    let start = detail.find(&needle)? + needle.len();
    let remainder = &detail[start..];
    let end = remainder
        .find(char::is_whitespace)
        .unwrap_or(remainder.len());
    Some(remainder[..end].trim_matches('"'))
}

fn parse_usize(raw: &str) -> Option<usize> {
    raw.parse::<usize>().ok()
}

fn truncate_detail(detail: &str, limit: usize) -> String {
    let normalized = detail.split_whitespace().collect::<Vec<_>>().join(" ");
    if normalized.chars().count() <= limit {
        return normalized;
    }

    let mut truncated = String::new();
    for ch in normalized.chars().take(limit.saturating_sub(1)) {
        truncated.push(ch);
    }
    truncated.push('…');
    truncated
}

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

    #[test]
    fn restart_churn_detects_requested_families() {
        let mut state = RestartChurnState::default();
        state.observe(
            "codex_start",
            "codex_start mode=fresh_restart restart_count=2",
        );
        state.observe(
            "auto_trigger_timeout",
            "auto_trigger_timeout harness=codex reason=no_prompt_after_30s",
        );
        state.observe(
            "ctrl_d_restart_fresh",
            "ctrl_d_restart_fresh restart_count=3",
        );
        state.observe("user_quit_after_ctrl_d", "user_quit_after_ctrl_d");
        state.observe(
            "supervisor_exit",
            "supervisor_exit reason=user_quit_after_ctrl_d pane=%26 restart_count=0",
        );

        let summaries = state.summaries();
        assert_eq!(state.groups(), 4);
        assert!(summaries.iter().any(|entry| entry.family == "fresh_restart"
            && entry.occurrences == 2
            && entry.max_restart_count == Some(3)));
        assert!(
            summaries
                .iter()
                .any(|entry| entry.family == "auto_trigger_timeout" && entry.occurrences == 1)
        );
        assert!(
            summaries
                .iter()
                .any(|entry| entry.family == "ctrl_d_restart_loop"
                    && entry.occurrences == 1
                    && entry.max_restart_count == Some(3))
        );
        assert!(
            summaries
                .iter()
                .any(|entry| entry.family == "quit_after_eof" && entry.occurrences == 2)
        );
        assert!(!is_restart_churn_warning_family("quit_after_eof"));
        assert!(is_restart_churn_warning_family("fresh_restart"));
    }
}
