use anyhow::Result;
use auditaur_core::{
    model::{
        FrontendError, LogRecord, SpanRecord, TauriEventRecord, TauriIpcCall, TauriWindowState,
        TelemetrySource,
    },
    storage::{RelatedTelemetry, RelatedTelemetryQuery},
};
use serde::Serialize;
use serde_json::{json, Value};
use std::{
    collections::HashSet,
    fs,
    path::PathBuf,
    thread,
    time::{Duration, Instant},
};

use crate::{commands::read, discovery, output::table_cell};

pub fn timeline(
    db: &Option<PathBuf>,
    session_id: Option<String>,
    trace_id: Option<String>,
    since: Option<String>,
    json: bool,
    limit: usize,
) -> Result<()> {
    let db = discovery::resolve_db(db.clone())?;
    let mut entries = load_timeline(&db, session_id, trace_id, since.as_deref(), limit)?;
    entries.truncate(limit);
    read::print_json_or_table(json, &entries, || print_timeline(&entries))
}

pub fn related(
    db: &Option<PathBuf>,
    session_id: Option<String>,
    trace_id: Option<String>,
    run_id: Option<String>,
    window_label: Option<String>,
    since: Option<String>,
    json: bool,
    limit: usize,
) -> Result<()> {
    let db = discovery::resolve_db(db.clone())?;
    let trace_id = resolve_related_trace(&db, session_id.as_deref(), trace_id, run_id)?;
    let related = load_related(
        &db,
        session_id,
        trace_id,
        window_label,
        since.as_deref(),
        limit,
    )?;
    read::print_json_or_table(json, &related, || print_related(&related))
}

fn resolve_related_trace(
    db: &PathBuf,
    session_id: Option<&str>,
    trace_id: Option<String>,
    run_id: Option<String>,
) -> Result<Option<String>> {
    if trace_id.is_some() || run_id.is_none() {
        return Ok(trace_id);
    }
    let run_id = run_id.expect("checked above");
    let store = read::open_validated_store(db)?;
    crate::commands::agent::find_run_trace_id(&store, session_id, &run_id)?
        .map(Some)
        .ok_or_else(|| anyhow::anyhow!("No agent run `{run_id}` found. Try `auditaur agent-runs`."))
}

pub fn explain(
    db: &Option<PathBuf>,
    session_id: Option<String>,
    trace_id: Option<String>,
    since: Option<String>,
    json: bool,
    limit: usize,
) -> Result<()> {
    let db = discovery::resolve_db(db.clone())?;
    let related = load_related(
        &db,
        session_id,
        trace_id.clone(),
        None,
        since.as_deref(),
        limit,
    )?;
    let entries = timeline_entries(related.clone(), limit);
    let report = ExplainReport::from_related(trace_id, &related, &entries);
    read::print_json_or_table(json, &report, || print_explain(&report))
}

pub fn bundle(
    db: &Option<PathBuf>,
    session_id: Option<String>,
    trace_id: Option<String>,
    since: Option<String>,
    _redacted: bool,
    output: Option<PathBuf>,
    limit: usize,
) -> Result<()> {
    let db = discovery::resolve_db(db.clone())?;
    let store = read::open_validated_store(&db)?;
    let sessions = store.list_sessions(Some(limit))?;
    let related = related_from_store(&store, session_id, trace_id, None, since.as_deref(), limit)?;
    let mut bundle = json!({
        "schemaVersion": 1,
        "generatedAtUnixNanos": read::current_time_unix_nanos(),
        "databasePath": db,
        "redacted": true,
        "sessions": sessions,
        "logs": related.logs,
        "spans": related.spans,
        "spanEvents": related.span_events,
        "frontendErrors": related.frontend_errors,
        "tauriIpcCalls": related.tauri_ipc_calls,
        "tauriEvents": related.tauri_events,
        "tauriWindows": related.tauri_windows,
    });
    redact_value(&mut bundle);
    let serialized = serde_json::to_string_pretty(&bundle)?;
    if let Some(output) = output {
        fs::write(output, serialized)?;
    } else {
        println!("{serialized}");
    }
    Ok(())
}

pub fn tail(
    db: &Option<PathBuf>,
    session_id: Option<String>,
    trace_id: Option<String>,
    replay: bool,
    interval_ms: u64,
    duration_seconds: Option<u64>,
    json: bool,
) -> Result<()> {
    let db = discovery::resolve_db(db.clone())?;
    let started = Instant::now();
    let mut last_seen = if replay {
        0
    } else {
        read::current_time_unix_nanos()
    };
    loop {
        let mut entries =
            load_timeline(&db, session_id.clone(), trace_id.clone(), None, usize::MAX)?;
        entries.retain(|entry| entry.timestamp_unix_nanos > last_seen);
        entries.sort_by_key(|entry| entry.timestamp_unix_nanos);
        for entry in &entries {
            last_seen = last_seen.max(entry.timestamp_unix_nanos);
            if json {
                println!("{}", serde_json::to_string(entry)?);
            } else {
                print_timeline_entry(entry);
            }
        }
        if duration_seconds.is_some_and(|seconds| started.elapsed() >= Duration::from_secs(seconds))
        {
            break;
        }
        thread::sleep(Duration::from_millis(interval_ms.max(100)));
    }
    Ok(())
}

fn load_timeline(
    db: &PathBuf,
    session_id: Option<String>,
    trace_id: Option<String>,
    since: Option<&str>,
    limit: usize,
) -> Result<Vec<TimelineEntry>> {
    let related = load_related(db, session_id, trace_id, None, since, limit)?;
    Ok(timeline_entries(related, limit))
}

fn load_related(
    db: &PathBuf,
    session_id: Option<String>,
    trace_id: Option<String>,
    window_label: Option<String>,
    since: Option<&str>,
    limit: usize,
) -> Result<RelatedTelemetry> {
    let store = read::open_validated_store(db)?;
    related_from_store(&store, session_id, trace_id, window_label, since, limit)
}

pub(crate) fn related_from_store(
    store: &auditaur_collector::exporter_sqlite::SqliteStore,
    session_id: Option<String>,
    trace_id: Option<String>,
    window_label: Option<String>,
    since: Option<&str>,
    limit: usize,
) -> Result<RelatedTelemetry> {
    let start_time_unix_nanos = read::parse_since_cutoff(since)?;
    Ok(store.related_telemetry(&RelatedTelemetryQuery {
        session_id,
        trace_id,
        window_label,
        start_time_unix_nanos,
        end_time_unix_nanos: None,
        limit: Some(limit),
    })?)
}

fn timeline_entries(related: RelatedTelemetry, limit: usize) -> Vec<TimelineEntry> {
    let mut entries = Vec::new();
    entries.extend(related.logs.into_iter().map(TimelineEntry::from_log));
    entries.extend(related.spans.into_iter().map(TimelineEntry::from_span));
    entries.extend(
        related
            .span_events
            .into_iter()
            .map(TimelineEntry::from_span_event),
    );
    entries.extend(
        related
            .frontend_errors
            .into_iter()
            .map(TimelineEntry::from_error),
    );
    entries.extend(
        related
            .tauri_ipc_calls
            .into_iter()
            .map(TimelineEntry::from_ipc),
    );
    entries.extend(
        related
            .tauri_events
            .into_iter()
            .map(TimelineEntry::from_event),
    );
    entries.extend(
        related
            .tauri_windows
            .into_iter()
            .map(TimelineEntry::from_window),
    );
    entries.sort_by_key(|entry| entry.timestamp_unix_nanos);
    if entries.len() > limit {
        entries = entries.split_off(entries.len() - limit);
    }
    entries
}

fn print_timeline(entries: &[TimelineEntry]) -> Result<()> {
    println!("TIME\tTYPE\tSTATUS\tTRACE\tSUMMARY");
    for entry in entries {
        print_timeline_entry(entry);
    }
    Ok(())
}

fn print_timeline_entry(entry: &TimelineEntry) {
    println!(
        "{}\t{}\t{}\t{}\t{}",
        entry.timestamp_unix_nanos,
        table_cell(&entry.kind, 20),
        table_cell(entry.status.as_deref().unwrap_or("-"), 20),
        table_cell(entry.trace_id.as_deref().unwrap_or("-"), 80),
        table_cell(&entry.summary, 220)
    );
}

fn print_explain(report: &ExplainReport) -> Result<()> {
    println!("Auditaur explain");
    println!("Total events: {}", report.total_events);
    println!("Errors: {}", report.error_count);
    println!("Failed IPC calls: {}", report.failed_ipc_count);
    println!("Failed spans: {}", report.failed_span_count);
    if report.findings.is_empty() {
        println!("No obvious failures found in the selected telemetry.");
    } else {
        println!("Findings:");
        for finding in &report.findings {
            println!("- {}", table_cell(finding, 240));
        }
    }
    Ok(())
}

fn print_related(related: &RelatedTelemetry) -> Result<()> {
    println!("TYPE\tCOUNT");
    println!("spans\t{}", related.spans.len());
    println!("span_events\t{}", related.span_events.len());
    println!("logs\t{}", related.logs.len());
    println!("frontend_errors\t{}", related.frontend_errors.len());
    println!("tauri_ipc_calls\t{}", related.tauri_ipc_calls.len());
    println!("tauri_events\t{}", related.tauri_events.len());
    println!("tauri_windows\t{}", related.tauri_windows.len());
    Ok(())
}

fn redact_value(value: &mut Value) {
    match value {
        Value::Object(map) => {
            for (key, value) in map.iter_mut() {
                let key_lower = key.to_ascii_lowercase();
                if key_lower.contains("secret")
                    || key_lower.contains("password")
                    || key_lower.contains("token")
                    || key_lower.contains("authorization")
                    || key_lower.contains("cookie")
                    || key_lower.ends_with("key")
                    || key_lower.ends_with("json")
                    || key_lower == "attributes"
                {
                    *value = Value::String("[redacted]".to_string());
                } else {
                    redact_value(value);
                }
            }
        }
        Value::Array(items) => {
            for item in items {
                redact_value(item);
            }
        }
        _ => {}
    }
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct TimelineEntry {
    timestamp_unix_nanos: i64,
    kind: String,
    session_id: String,
    trace_id: Option<String>,
    span_id: Option<String>,
    status: Option<String>,
    summary: String,
}

impl TimelineEntry {
    fn from_log(log: LogRecord) -> Self {
        Self {
            timestamp_unix_nanos: log.timestamp_unix_nanos,
            kind: "log".to_string(),
            session_id: log.session_id,
            trace_id: log.trace_id,
            span_id: log.span_id,
            status: log.severity_text,
            summary: log.body.unwrap_or_default(),
        }
    }

    fn from_span(span: SpanRecord) -> Self {
        Self {
            timestamp_unix_nanos: span.start_time_unix_nanos,
            kind: "span".to_string(),
            session_id: span.session_id,
            trace_id: Some(span.trace_id),
            span_id: Some(span.span_id),
            status: span.status_code,
            summary: span.name,
        }
    }

    fn from_span_event(event: auditaur_core::model::SpanEventRecord) -> Self {
        Self {
            timestamp_unix_nanos: event.timestamp_unix_nanos,
            kind: "span_event".to_string(),
            session_id: event.session_id,
            trace_id: Some(event.trace_id),
            span_id: Some(event.span_id),
            status: None,
            summary: event.name,
        }
    }

    fn from_error(error: FrontendError) -> Self {
        Self {
            timestamp_unix_nanos: error.timestamp_unix_nanos,
            kind: "frontend_error".to_string(),
            session_id: error.session_id,
            trace_id: error.trace_id,
            span_id: error.span_id,
            status: error.error_type,
            summary: error.message,
        }
    }

    fn from_ipc(call: TauriIpcCall) -> Self {
        let summary = match call.error_message {
            Some(error) => format!("{}: {error}", call.command),
            None => call.command,
        };
        Self {
            timestamp_unix_nanos: call.timestamp_unix_nanos,
            kind: "ipc".to_string(),
            session_id: call.session_id,
            trace_id: call.trace_id,
            span_id: call.span_id,
            status: Some(call.status),
            summary,
        }
    }

    fn from_event(event: TauriEventRecord) -> Self {
        Self {
            timestamp_unix_nanos: event.timestamp_unix_nanos,
            kind: "event".to_string(),
            session_id: event.session_id,
            trace_id: event.trace_id,
            span_id: event.span_id,
            status: Some(event.direction),
            summary: event.event_name,
        }
    }

    fn from_window(window: TauriWindowState) -> Self {
        let event = window
            .attributes
            .get("tauri.window.event")
            .and_then(serde_json::Value::as_str)
            .map(ToString::to_string);
        Self {
            timestamp_unix_nanos: window.timestamp_unix_nanos,
            kind: "window".to_string(),
            session_id: window.session_id,
            trace_id: None,
            span_id: None,
            status: event.or_else(|| {
                window.focused.map(|focused| {
                    if focused {
                        "focused".to_string()
                    } else {
                        "unfocused".to_string()
                    }
                })
            }),
            summary: window.window_label,
        }
    }
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct ExplainReport {
    trace_id: Option<String>,
    total_events: usize,
    error_count: usize,
    failed_ipc_count: usize,
    failed_span_count: usize,
    findings: Vec<String>,
}

impl ExplainReport {
    fn from_related(
        trace_id: Option<String>,
        related: &RelatedTelemetry,
        entries: &[TimelineEntry],
    ) -> Self {
        let mut report = Self::from_timeline(trace_id, entries);
        let mut continuation_findings = Self::missing_backend_continuation_findings(related);
        continuation_findings.extend(report.findings);
        report.findings = continuation_findings;
        report.findings.truncate(20);
        report
    }

    fn from_timeline(trace_id: Option<String>, entries: &[TimelineEntry]) -> Self {
        let mut findings = Vec::new();
        let mut error_count = 0;
        let mut failed_ipc_count = 0;
        let mut failed_span_count = 0;
        for entry in entries {
            match entry.kind.as_str() {
                "frontend_error" => {
                    error_count += 1;
                    findings.push(format!("Frontend error: {}", entry.summary));
                }
                "ipc"
                    if entry
                        .status
                        .as_deref()
                        .is_some_and(read::is_failed_ipc_status) =>
                {
                    failed_ipc_count += 1;
                    findings.push(format!("Failed IPC call: {}", entry.summary));
                }
                "span"
                    if entry
                        .status
                        .as_deref()
                        .is_some_and(|status| !status.eq_ignore_ascii_case("OK")) =>
                {
                    failed_span_count += 1;
                    findings.push(format!("Failed span: {}", entry.summary));
                }
                "log" if entry.status.as_deref().is_some_and(is_error_level) => {
                    error_count += 1;
                    findings.push(format!("Error log: {}", entry.summary));
                }
                _ => {}
            }
        }
        Self {
            trace_id,
            total_events: entries.len(),
            error_count,
            failed_ipc_count,
            failed_span_count,
            findings,
        }
    }

    fn missing_backend_continuation_findings(related: &RelatedTelemetry) -> Vec<String> {
        let mut findings = Vec::new();
        let mut reported = HashSet::new();
        for call in &related.tauri_ipc_calls {
            if call.command.contains(':') || call.command.contains('|') {
                continue;
            }
            let Some(trace_id) = call.trace_id.as_deref() else {
                continue;
            };
            let Some(frontend_span_id) = call.span_id.as_deref() else {
                continue;
            };
            let has_frontend_invoke_span = related.spans.iter().any(|span| {
                span.trace_id == trace_id
                    && span.span_id == frontend_span_id
                    && span.name == format!("tauri.invoke {}", call.command)
            });
            if !has_frontend_invoke_span {
                continue;
            }
            let has_backend_child_span = related.spans.iter().any(|span| {
                span.trace_id == trace_id
                    && span.source == TelemetrySource::Backend
                    && span.parent_span_id.as_deref() == Some(frontend_span_id)
            });
            if has_backend_child_span {
                continue;
            }
            if reported.insert((trace_id.to_string(), call.command.clone())) {
                findings.push(format!(
                    "Missing backend trace continuation for tauri.invoke {}; add #[tauri_plugin_auditaur::auditaur_command] or #[tauri_plugin_auditaur::instrument_ipc] to the Tauri command.",
                    call.command
                ));
            }
        }
        findings
    }
}

fn is_error_level(level: &str) -> bool {
    matches!(
        level.to_ascii_uppercase().as_str(),
        "ERROR" | "FATAL" | "CRITICAL"
    )
}
