fn set_status_light(light: &gtk::Box, state: StatusLightState) {
    light.remove_css_class("status-light-live");
    light.remove_css_class("status-light-connected");
    light.remove_css_class("status-light-idle");
    light.remove_css_class("status-light-warning");
    light.remove_css_class("status-light-caution");
    light.add_css_class(state.css_class());
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SessionLogSeverity {
    Error,
    Warn,
    Info,
    Debug,
    Trace,
    Other,
}

impl SessionLogSeverity {
    const fn rank(self) -> Option<u8> {
        match self {
            Self::Error => Some(0),
            Self::Warn => Some(1),
            Self::Info => Some(2),
            Self::Debug => Some(3),
            Self::Trace => Some(4),
            Self::Other => None,
        }
    }
}

/// Returns whether a cleaned log line should be visible at the selected threshold.
fn should_show_clean_session_log_line(message: &str, level: ConsoleLogLevel) -> bool {
    let severity = session_log_severity(message);
    if severity == SessionLogSeverity::Error {
        return true;
    }
    if message.starts_with("[launcher]") || message.starts_with("[preview:") {
        return true;
    }
    match severity.rank() {
        Some(rank) => rank <= level.rank(),
        None => true,
    }
}

/// Infers severity from the structured relay prefix and human-readable text.
fn session_log_severity(message: &str) -> SessionLogSeverity {
    let uppercase = message.to_ascii_uppercase();
    if message.starts_with("[relay:stderr]")
        || message.contains('❌')
        || uppercase.contains(" ERROR ")
        || uppercase.contains("FAILED")
        || uppercase.contains("PANIC")
        || uppercase.contains(" RPC FAILED")
    {
        SessionLogSeverity::Error
    } else if uppercase.contains(" WARN ")
        || uppercase.contains("UNAVAILABLE")
        || uppercase.contains("WAITING FOR CAPTURE PIPELINE")
    {
        SessionLogSeverity::Warn
    } else if uppercase.contains(" INFO ") {
        SessionLogSeverity::Info
    } else if uppercase.contains(" DEBUG ") {
        SessionLogSeverity::Debug
    } else if uppercase.contains(" TRACE ") {
        SessionLogSeverity::Trace
    } else {
        SessionLogSeverity::Other
    }
}

fn classify_log_tags(message: &str) -> Vec<&'static str> {
    let mut tags = Vec::new();
    if message.starts_with("[launcher]") {
        tags.push("log-launcher");
    } else if message.starts_with("[relay:stderr]") {
        tags.push("log-stderr");
    } else if message.starts_with("[relay]") {
        tags.push("log-relay");
    } else if message.starts_with("[preview:") {
        tags.push("log-preview");
    } else {
        tags.push("log-launcher");
    }

    let uppercase = message.to_ascii_uppercase();
    if message.starts_with("[relay:stderr]")
        || message.contains('❌')
        || uppercase.contains(" ERROR ")
        || uppercase.contains("FAILED")
        || uppercase.contains("PANIC")
        || uppercase.contains(" RPC FAILED")
    {
        tags.push("log-error");
    } else if uppercase.contains(" WARN ")
        || uppercase.contains("UNAVAILABLE")
        || uppercase.contains("WAITING FOR CAPTURE PIPELINE")
    {
        tags.push("log-warn");
    }
    tags
}

fn strip_ansi_sequences(input: &str) -> String {
    let mut output = String::with_capacity(input.len());
    let mut chars = input.chars().peekable();
    while let Some(ch) = chars.next() {
        if ch == '\u{1b}' {
            match chars.peek().copied() {
                Some('[') => {
                    let _ = chars.next();
                    for next in chars.by_ref() {
                        if ('@'..='~').contains(&next) {
                            break;
                        }
                    }
                }
                Some(']') => {
                    let _ = chars.next();
                    while let Some(next) = chars.next() {
                        if next == '\u{7}' {
                            break;
                        }
                        if next == '\u{1b}' && matches!(chars.peek(), Some('\\')) {
                            let _ = chars.next();
                            break;
                        }
                    }
                }
                _ => {}
            }
            continue;
        }
        output.push(ch);
    }
    output
}
