use std::{
    io::{Read, Write},
    net::TcpStream,
    path::{Path, PathBuf},
    process::{Command, Stdio},
    thread,
    time::{Duration, Instant},
};

#[cfg(windows)]
use std::os::windows::process::CommandExt;

use anyhow::{anyhow, Context, Result};
use auditaur_core::{
    model::TelemetrySource,
    storage::{
        FrontendErrorQuery, LogQuery, SpanEventQuery, SpanQuery, TauriEventQuery, TauriIpcQuery,
        TauriWindowQuery,
    },
};
use serde::Serialize;

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

#[derive(Debug, Clone)]
pub struct DebugSelector {
    pub db: Option<PathBuf>,
    pub app: Option<String>,
    pub session_id: Option<String>,
    pub instance_id: Option<String>,
    pub pid: Option<u32>,
    pub latest: bool,
    pub active: bool,
    pub cdp_port: Option<u16>,
    pub require_frontend: bool,
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DebugStatus {
    pub schema_version: u8,
    pub generated_at_unix_nanos: i64,
    pub ready: bool,
    pub app: Option<DiscoveredApp>,
    pub database_path: Option<String>,
    pub session_id: Option<String>,
    pub stages: Vec<DebugStage>,
    pub telemetry: DebugTelemetryCounts,
    pub cdp: Option<DebugCdpStatus>,
    pub hints: Vec<String>,
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct DebugStage {
    pub name: String,
    pub status: DebugStageStatus,
    pub message: String,
}

#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum DebugStageStatus {
    Ok,
    Waiting,
    Error,
    Skipped,
}

#[derive(Debug, Clone, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DebugTelemetryCounts {
    pub sessions: usize,
    pub logs: usize,
    pub spans: usize,
    pub span_events: usize,
    pub frontend_errors: usize,
    pub ipc: usize,
    pub events: usize,
    pub windows: usize,
    pub frontend_records: usize,
    pub backend_records: usize,
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DebugCdpStatus {
    pub port: u16,
    pub ok: bool,
    pub target_count: Option<usize>,
    pub browser: Option<String>,
    pub message: String,
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct DebugRunResult {
    status: DebugStatus,
    process: DebugProcessStatus,
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct DebugProcessStatus {
    command: Vec<String>,
    pid: Option<u32>,
    exit_code: Option<i32>,
    running: bool,
}

pub fn status(selector: DebugSelector, json: bool) -> Result<()> {
    let status = snapshot(&selector)?;
    read::print_json_or_table(json, &status, || print_status(&status))
}

pub fn watch(
    selector: DebugSelector,
    interval_ms: u64,
    timeout_seconds: Option<u64>,
    until_ready: bool,
    json: bool,
) -> Result<()> {
    let started = Instant::now();
    let interval = Duration::from_millis(interval_ms).max(Duration::from_millis(100));
    loop {
        let status = snapshot(&selector)?;
        if json {
            println!("{}", serde_json::to_string(&status)?);
        } else {
            print_status(&status)?;
            println!();
        }
        if until_ready && status.ready {
            return Ok(());
        }
        if !until_ready {
            return Ok(());
        }
        if timeout_seconds.is_some_and(|seconds| started.elapsed() >= Duration::from_secs(seconds))
        {
            return Err(anyhow!(
                "Timed out after {}s waiting for Auditaur debug readiness.",
                timeout_seconds.unwrap()
            ));
        }
        thread::sleep(interval);
    }
}

pub fn run(
    selector: DebugSelector,
    interval_ms: u64,
    timeout_seconds: Option<u64>,
    json: bool,
    command: Vec<String>,
) -> Result<()> {
    if command.is_empty() {
        return Err(anyhow!(
            "`auditaur debug run` requires a command after `--`."
        ));
    }
    let mut child_command = Command::new(&command[0]);
    child_command.args(&command[1..]);
    if json {
        child_command.stdout(Stdio::null()).stderr(Stdio::null());
        #[cfg(windows)]
        child_command.creation_flags(0x0800_0000);
    } else {
        child_command
            .stdin(Stdio::inherit())
            .stdout(Stdio::inherit())
            .stderr(Stdio::inherit());
    }
    let mut child = child_command
        .spawn()
        .with_context(|| format!("failed to start `{}`", command.join(" ")))?;
    let pid = child.id();
    let started = Instant::now();
    let interval = Duration::from_millis(interval_ms).max(Duration::from_millis(100));
    let mut final_status;
    loop {
        final_status = match snapshot(&selector) {
            Ok(status) => status,
            Err(error) => {
                let _ = child.kill();
                let _ = child.wait();
                return Err(error.context("debug readiness snapshot failed after starting command"));
            }
        };
        if json {
            println!("{}", serde_json::to_string(&final_status)?);
        } else {
            print_status(&final_status)?;
            println!();
        }
        if final_status.ready {
            break;
        }
        if let Some(status) = child.try_wait()? {
            let result = DebugRunResult {
                status: final_status,
                process: DebugProcessStatus {
                    command,
                    pid: Some(pid),
                    exit_code: status.code(),
                    running: false,
                },
            };
            if json {
                println!("{}", serde_json::to_string(&result)?);
            }
            return Err(anyhow!(
                "debug command exited before Auditaur became ready with status {status}"
            ));
        }
        if timeout_seconds.is_some_and(|seconds| started.elapsed() >= Duration::from_secs(seconds))
        {
            let _ = child.kill();
            let _ = child.wait();
            return Err(anyhow!(
                "Timed out after {}s waiting for Auditaur debug readiness.",
                timeout_seconds.unwrap()
            ));
        }
        thread::sleep(interval);
    }

    let child_status = child.try_wait()?;
    let process = DebugProcessStatus {
        command,
        pid: Some(pid),
        exit_code: child_status.and_then(|status| status.code()),
        running: child_status.is_none(),
    };
    let result = DebugRunResult {
        status: final_status,
        process,
    };
    if json {
        println!("{}", serde_json::to_string(&result)?);
    } else {
        println!("Auditaur debug session ready.");
    }
    Ok(())
}

fn snapshot(selector: &DebugSelector) -> Result<DebugStatus> {
    let mut stages = Vec::new();
    let mut hints = Vec::new();
    let app = if selector.db.is_some() {
        stages.push(stage(
            "app_discovery",
            DebugStageStatus::Skipped,
            "using explicit --db; app discovery is not required",
        ));
        stages.push(stage(
            "heartbeat",
            DebugStageStatus::Skipped,
            "using explicit --db; app heartbeat is not required",
        ));
        None
    } else {
        match select_app(selector)? {
            Some(app) => {
                stages.push(stage(
                    "app_discovery",
                    DebugStageStatus::Ok,
                    format!(
                        "discovered {} pid={} session={}",
                        app.service_name, app.pid, app.session_id
                    ),
                ));
                Some(app)
            }
            None => {
                stages.push(stage(
                    "app_discovery",
                    DebugStageStatus::Waiting,
                    "waiting for an Auditaur discovery file",
                ));
                hints.push(
                    "Start the app or use `auditaur debug run -- <command>` to wrap startup."
                        .to_string(),
                );
                None
            }
        }
    };

    let db = selector
        .db
        .clone()
        .or_else(|| app.as_ref().map(|app| PathBuf::from(&app.database_path)));
    if let Some(app) = &app {
        if app.status == DiscoveryStatus::Active {
            stages.push(stage(
                "heartbeat",
                DebugStageStatus::Ok,
                format!(
                    "heartbeat is fresh{}",
                    app.heartbeat_age_seconds
                        .map(|age| format!(" ({age}s old)"))
                        .unwrap_or_default()
                ),
            ));
        } else {
            stages.push(stage(
                "heartbeat",
                DebugStageStatus::Waiting,
                app.stale_reason
                    .clone()
                    .unwrap_or_else(|| "heartbeat is stale".to_string()),
            ));
        }
    }

    let (database_path, telemetry, session_id) = match db {
        Some(db) => load_database_status(&db, selector, &mut stages, &mut hints)?,
        None => {
            stages.push(stage(
                "telemetry_database",
                DebugStageStatus::Waiting,
                "waiting for a telemetry database path",
            ));
            (None, DebugTelemetryCounts::default(), None)
        }
    };

    if let Some(cdp_port) = selector.cdp_port {
        let cdp = cdp_status(cdp_port);
        stages.push(stage(
            "cdp_endpoint",
            if cdp.ok {
                DebugStageStatus::Ok
            } else {
                DebugStageStatus::Waiting
            },
            cdp.message.clone(),
        ));
        let ready = readiness(&stages, selector.require_frontend);
        Ok(DebugStatus {
            schema_version: 1,
            generated_at_unix_nanos: read::current_time_unix_nanos(),
            ready,
            app,
            database_path,
            session_id,
            stages,
            telemetry,
            cdp: Some(cdp),
            hints,
        })
    } else {
        stages.push(stage(
            "cdp_endpoint",
            DebugStageStatus::Skipped,
            "pass --cdp-port to include WebView/CDP readiness",
        ));
        let ready = readiness(&stages, selector.require_frontend);
        Ok(DebugStatus {
            schema_version: 1,
            generated_at_unix_nanos: read::current_time_unix_nanos(),
            ready,
            app,
            database_path,
            session_id,
            stages,
            telemetry,
            cdp: None,
            hints,
        })
    }
}

fn load_database_status(
    db: &Path,
    selector: &DebugSelector,
    stages: &mut Vec<DebugStage>,
    hints: &mut Vec<String>,
) -> Result<(Option<String>, DebugTelemetryCounts, Option<String>)> {
    if !db.is_file() {
        stages.push(stage(
            "telemetry_database",
            DebugStageStatus::Waiting,
            format!("waiting for telemetry database: {}", db.display()),
        ));
        return Ok((
            Some(db.to_string_lossy().to_string()),
            DebugTelemetryCounts::default(),
            None,
        ));
    }

    let store = match read::open_validated_store(db) {
        Ok(store) => {
            stages.push(stage(
                "telemetry_database",
                DebugStageStatus::Ok,
                format!("database is readable and schema-valid: {}", db.display()),
            ));
            store
        }
        Err(error) => {
            stages.push(stage(
                "telemetry_database",
                DebugStageStatus::Error,
                format!("database could not be opened or validated: {error}"),
            ));
            return Ok((
                Some(db.to_string_lossy().to_string()),
                DebugTelemetryCounts::default(),
                None,
            ));
        }
    };

    let sessions = store.list_sessions(Some(50))?;
    let session_id = selector
        .session_id
        .clone()
        .or_else(|| sessions.first().map(|session| session.id.clone()));
    if let Some(session_id) = &session_id {
        stages.push(stage(
            "session",
            DebugStageStatus::Ok,
            format!("session is queryable: {session_id}"),
        ));
    } else {
        stages.push(stage(
            "session",
            DebugStageStatus::Waiting,
            "waiting for the first session row",
        ));
    }

    let telemetry = telemetry_counts(&store, session_id.clone())?;
    if telemetry.windows > 0 {
        stages.push(stage(
            "window",
            DebugStageStatus::Ok,
            format!("{} window record(s) captured", telemetry.windows),
        ));
    } else {
        stages.push(stage(
            "window",
            DebugStageStatus::Waiting,
            "waiting for Tauri window telemetry",
        ));
    }

    if telemetry.backend_records > 0 {
        stages.push(stage(
            "backend_telemetry",
            DebugStageStatus::Ok,
            format!(
                "{} backend/plugin record(s) captured",
                telemetry.backend_records
            ),
        ));
    } else {
        stages.push(stage(
            "backend_telemetry",
            DebugStageStatus::Waiting,
            "waiting for backend/plugin telemetry rows",
        ));
    }

    if telemetry.frontend_records > 0 {
        stages.push(stage(
            "frontend_telemetry",
            DebugStageStatus::Ok,
            format!("{} frontend record(s) captured", telemetry.frontend_records),
        ));
    } else {
        stages.push(stage(
            "frontend_telemetry",
            if selector.require_frontend {
                DebugStageStatus::Waiting
            } else {
                DebugStageStatus::Skipped
            },
            "no frontend telemetry rows observed yet",
        ));
        hints.push(
            "If frontend telemetry is expected, click a webview action and inspect app UI/export errors."
                .to_string(),
        );
    }

    Ok((
        Some(db.to_string_lossy().to_string()),
        DebugTelemetryCounts {
            sessions: sessions.len(),
            ..telemetry
        },
        session_id,
    ))
}

fn telemetry_counts(
    store: &auditaur_collector::exporter_sqlite::SqliteStore,
    session_id: Option<String>,
) -> Result<DebugTelemetryCounts> {
    let logs = store.list_logs(&LogQuery {
        session_id: session_id.clone(),
        trace_id: None,
        limit: Some(usize::MAX),
    })?;
    let spans = store.list_spans(&SpanQuery {
        session_id: session_id.clone(),
        trace_id: None,
        limit: Some(usize::MAX),
    })?;
    let span_events = store.list_span_events(&SpanEventQuery {
        session_id: session_id.clone(),
        trace_id: None,
        limit: Some(usize::MAX),
    })?;
    let frontend_errors = store.list_frontend_errors(&FrontendErrorQuery {
        session_id: session_id.clone(),
        trace_id: None,
        limit: Some(usize::MAX),
    })?;
    let ipc = store.list_tauri_ipc_calls(&TauriIpcQuery {
        session_id: session_id.clone(),
        trace_id: None,
        limit: Some(usize::MAX),
    })?;
    let events = store.list_tauri_events(&TauriEventQuery {
        session_id: session_id.clone(),
        trace_id: None,
        limit: Some(usize::MAX),
    })?;
    let windows = store.list_tauri_windows(&TauriWindowQuery {
        session_id,
        latest_only: false,
        limit: Some(usize::MAX),
    })?;
    let frontend_logs = logs
        .iter()
        .filter(|log| log.source == TelemetrySource::Frontend)
        .count();
    let backend_logs = logs.len().saturating_sub(frontend_logs);
    let frontend_spans = spans
        .iter()
        .filter(|span| span.source == TelemetrySource::Frontend)
        .count();
    let backend_spans = spans.len().saturating_sub(frontend_spans);
    Ok(DebugTelemetryCounts {
        sessions: 0,
        logs: logs.len(),
        spans: spans.len(),
        span_events: span_events.len(),
        frontend_errors: frontend_errors.len(),
        ipc: ipc.len(),
        events: events.len(),
        windows: windows.len(),
        frontend_records: frontend_logs
            + frontend_spans
            + frontend_errors.len()
            + ipc.len()
            + events.len(),
        backend_records: backend_logs + backend_spans + span_events.len() + windows.len(),
    })
}

fn select_app(selector: &DebugSelector) -> Result<Option<DiscoveredApp>> {
    let mut candidates: Vec<_> = discovery::list_apps()?
        .into_iter()
        .filter(|candidate| {
            selector
                .app
                .as_deref()
                .is_none_or(|needle| app_matches(candidate, needle))
        })
        .filter(|candidate| {
            selector
                .session_id
                .as_deref()
                .is_none_or(|needle| candidate.session_id.contains(needle))
        })
        .filter(|candidate| {
            selector
                .instance_id
                .as_deref()
                .is_none_or(|needle| candidate.instance_id.contains(needle))
        })
        .filter(|candidate| selector.pid.is_none_or(|pid| candidate.pid == pid))
        .filter(|candidate| !selector.active || candidate.status == DiscoveryStatus::Active)
        .collect();
    candidates.sort_by(|left, right| {
        let left_active = left.status == DiscoveryStatus::Active;
        let right_active = right.status == DiscoveryStatus::Active;
        right_active
            .cmp(&left_active)
            .then_with(|| right.last_heartbeat_at.cmp(&left.last_heartbeat_at))
    });
    if selector.latest {
        return Ok(candidates.into_iter().next());
    }
    let active_count = candidates
        .iter()
        .filter(|candidate| candidate.status == DiscoveryStatus::Active)
        .count();
    match candidates.as_slice() {
        [] => Ok(None),
        [candidate] => Ok(Some(candidate.clone())),
        _ if active_count == 1 => Ok(candidates
            .into_iter()
            .find(|candidate| candidate.status == DiscoveryStatus::Active)),
        _ => Err(anyhow!(
            "Multiple Auditaur apps matched debug selector. Pass --session-id, --instance-id, --pid, --latest, or --active."
        )),
    }
}

fn app_matches(candidate: &DiscoveredApp, needle: &str) -> bool {
    let needle = needle.to_ascii_lowercase();
    candidate
        .service_name
        .to_ascii_lowercase()
        .contains(&needle)
        || candidate
            .app_identifier
            .as_deref()
            .is_some_and(|identifier| identifier.to_ascii_lowercase().contains(&needle))
        || candidate.session_id.to_ascii_lowercase().contains(&needle)
}

fn cdp_status(port: u16) -> DebugCdpStatus {
    match get_cdp_json(port, "/json/version")
        .and_then(|version| get_cdp_json(port, "/json/list").map(|targets| (version, targets)))
    {
        Ok((version, targets)) => {
            let target_count = targets.as_array().map(Vec::len);
            let browser = version
                .get("Browser")
                .and_then(|value| value.as_str())
                .map(str::to_string);
            DebugCdpStatus {
                port,
                ok: true,
                target_count,
                browser,
                message: format!(
                    "CDP endpoint is reachable on port {port} with {} target(s)",
                    target_count.unwrap_or_default()
                ),
            }
        }
        Err(error) => DebugCdpStatus {
            port,
            ok: false,
            target_count: None,
            browser: None,
            message: format!("waiting for CDP endpoint on port {port}: {error}"),
        },
    }
}

fn get_cdp_json(port: u16, path: &str) -> Result<serde_json::Value> {
    let mut stream = TcpStream::connect_timeout(
        &format!("127.0.0.1:{port}").parse()?,
        Duration::from_millis(500),
    )?;
    stream.set_read_timeout(Some(Duration::from_millis(500)))?;
    stream.set_write_timeout(Some(Duration::from_millis(500)))?;
    write!(
        stream,
        "GET {path} HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\nConnection: close\r\n\r\n"
    )?;
    let mut response = Vec::new();
    let mut byte = [0_u8; 1];
    while !response.ends_with(b"\r\n\r\n") {
        stream.read_exact(&mut byte)?;
        response.push(byte[0]);
    }
    let headers = String::from_utf8(response)?;
    let content_length = headers
        .lines()
        .find_map(|line| {
            let (name, value) = line.split_once(':')?;
            name.eq_ignore_ascii_case("content-length")
                .then(|| value.trim().parse::<usize>().ok())
                .flatten()
        })
        .ok_or_else(|| anyhow!("CDP response did not include Content-Length"))?;
    let mut body = vec![0_u8; content_length];
    stream.read_exact(&mut body)?;
    Ok(serde_json::from_slice(&body)?)
}

fn readiness(stages: &[DebugStage], require_frontend: bool) -> bool {
    let required = [
        "app_discovery",
        "heartbeat",
        "telemetry_database",
        "session",
        "window",
        "backend_telemetry",
        "cdp_endpoint",
    ];
    required.iter().all(|name| {
        stages
            .iter()
            .find(|stage| stage.name == *name)
            .is_some_and(|stage| {
                stage.status == DebugStageStatus::Ok || stage.status == DebugStageStatus::Skipped
            })
    }) && (!require_frontend
        || stages
            .iter()
            .find(|stage| stage.name == "frontend_telemetry")
            .is_some_and(|stage| stage.status == DebugStageStatus::Ok))
}

fn stage(
    name: impl Into<String>,
    status: DebugStageStatus,
    message: impl Into<String>,
) -> DebugStage {
    DebugStage {
        name: name.into(),
        status,
        message: message.into(),
    }
}

fn print_status(status: &DebugStatus) -> Result<()> {
    let ok = status
        .stages
        .iter()
        .filter(|stage| stage.status == DebugStageStatus::Ok)
        .count();
    let waiting = status
        .stages
        .iter()
        .filter(|stage| stage.status == DebugStageStatus::Waiting)
        .count();
    let errors = status
        .stages
        .iter()
        .filter(|stage| stage.status == DebugStageStatus::Error)
        .count();
    println!(
        "Auditaur debug: {} (ok={} waiting={} errors={})",
        if status.ready { "ready" } else { "waiting" },
        ok,
        waiting,
        errors
    );
    for stage in &status.stages {
        println!(
            "{}\t{:?}\t{}",
            table_cell(&stage.name, 24),
            stage.status,
            table_cell(&stage.message, 96)
        );
    }
    println!(
        "telemetry\tsessions={} logs={} spans={} span_events={} frontend_errors={} ipc={} events={} windows={}",
        status.telemetry.sessions,
        status.telemetry.logs,
        status.telemetry.spans,
        status.telemetry.span_events,
        status.telemetry.frontend_errors,
        status.telemetry.ipc,
        status.telemetry.events,
        status.telemetry.windows
    );
    for hint in &status.hints {
        println!("hint\t{}", table_cell(hint, 120));
    }
    Ok(())
}
