use std::process::Command;
use std::time::Duration;

use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use clap::Parser;
use reqwest::blocking::Client;
use serde_json::Value;

const DEFAULT_SERVER: &str = "http://192.0.2.13:5600";
const DEFAULT_HOST: &str = "HOST-EXAMPLE";
const BUCKETS: &[&str] = &[
    "aw-dlp-endpoint-signals",
    "aw-dlp-incidents",
    "aw-dlp-review",
    "aw-dlp-rules",
    "aw-watcher-afk",
    "aw-watcher-window",
    "aw-session-events",
    "aw-worktime-sessions",
];

#[derive(Debug, Parser)]
#[command(about = "Check ActivityWatch data collection freshness for DetMir/AW-RUS")]
struct Cli {
    #[arg(long)]
    server: Option<String>,

    #[arg(long)]
    host: Option<String>,

    #[arg(long, default_value_t = 15)]
    timeout_seconds: u64,

    #[arg(long, default_value_t = 15)]
    bucket_timeout_seconds: u64,

    #[arg(long, default_value_t = 3)]
    context_timeout_seconds: u64,

    #[arg(long, default_value_t = false)]
    with_event_ids: bool,

    #[arg(long, default_value_t = false)]
    no_color: bool,
}

#[derive(Debug, Clone)]
struct Colors {
    red: &'static str,
    green: &'static str,
    yellow: &'static str,
    cyan: &'static str,
    reset: &'static str,
}

impl Colors {
    fn new(enabled: bool) -> Self {
        if enabled {
            Self {
                red: "\u{1b}[0;31m",
                green: "\u{1b}[0;32m",
                yellow: "\u{1b}[1;33m",
                cyan: "\u{1b}[0;36m",
                reset: "\u{1b}[0m",
            }
        } else {
            Self {
                red: "",
                green: "",
                yellow: "",
                cyan: "",
                reset: "",
            }
        }
    }

    fn paint(&self, color: &str, text: &str) -> String {
        format!("{color}{text}{}", self.reset)
    }
}

#[derive(Debug, Default)]
struct ContextState {
    host_inactive: bool,
    guard_healthy: bool,
}

#[derive(Debug, PartialEq, Eq)]
enum BucketStatus {
    Fresh,
    Stale,
    Dead,
    Empty,
    EventDriven,
    Inactive,
    Unknown,
}

fn main() {
    let code = match run() {
        Ok(code) => code,
        Err(err) => {
            eprintln!("{err:#}");
            1
        }
    };
    std::process::exit(code);
}

fn run() -> Result<i32> {
    let cli = Cli::parse();
    let server = cli
        .server
        .or_else(|| env_nonempty("AW_CHECK_SERVER"))
        .or_else(|| env_nonempty("AW_SERVER_URL"))
        .unwrap_or_else(|| DEFAULT_SERVER.to_string())
        .trim_end_matches('/')
        .to_string();
    let host = cli
        .host
        .or_else(|| env_nonempty("AW_CHECK_HOST"))
        .or_else(|| env_nonempty("AW_MONITORED_HOST"))
        .or_else(|| env_nonempty("AW_MONITORED_WINDOWS_HOSTNAME"))
        .unwrap_or_else(|| DEFAULT_HOST.to_string());
    let colors = Colors::new(!cli.no_color && env_nonempty("NO_COLOR").is_none());
    let client = Client::builder()
        .no_proxy()
        .build()
        .context("build HTTP client")?;
    let now = Utc::now();

    println!("=== ActivityWatch Data Check: {host} ===");
    println!();
    print!("Server connectivity... ");
    let info_url = format!("{server}/api/0/info");
    let info = match get_json(&client, &info_url, cli.timeout_seconds) {
        Ok(value) => value,
        Err(err) => {
            eprintln!("check-aw-data: {err:#}");
            println!(
                "{} (cannot reach {server})",
                colors.paint(colors.red, "FAILED")
            );
            return Ok(1);
        }
    };
    let Some(version) = info.get("version").and_then(Value::as_str) else {
        println!(
            "{} (cannot reach {server})",
            colors.paint(colors.red, "FAILED")
        );
        return Ok(1);
    };
    println!("{} (aw-server {version})", colors.paint(colors.green, "OK"));
    println!();

    let context = read_context(&server, &host, now, cli.context_timeout_seconds);
    let buckets_index = get_json(
        &client,
        &format!("{server}/api/0/buckets"),
        cli.timeout_seconds,
    )
    .ok();
    println!("--- Buckets ---");
    println!(
        "{:<45} {:<8} {:<22} STATUS",
        "BUCKET", "EVENTS", "LAST EVENT"
    );
    println!(
        "{:<45} {:<8} {:<22} ------",
        "---------------------------------------------", "--------", "----------------------"
    );

    for bucket in BUCKETS {
        let bucket_full = format!("{bucket}_{host}");
        let event = bucket_event(
            &server,
            &bucket_full,
            buckets_index.as_ref(),
            cli.with_event_ids,
            cli.bucket_timeout_seconds,
        );
        let (last_id, last_ts, age, status) = render_bucket(bucket, event.as_ref(), now, &context);
        println!(
            "{:<45} {:<8} {:<22} {}",
            bucket_full,
            last_id,
            format!("{last_ts} ({age})"),
            render_status(&colors, status)
        );
    }

    println!();
    println!("--- CORS Check ---");
    let cors_status = check_cors(&client, &server);
    if cors_status == 200 {
        println!("{} (HTTP 200)", colors.paint(colors.green, "CORS: OK"));
    } else {
        println!(
            "{} (HTTP {cors_status})",
            colors.paint(colors.red, "CORS: FAIL")
        );
    }

    println!();
    println!("=== Check Complete ===");
    println!("Timestamp: {}", now.format("%Y-%m-%d %H:%M:%S UTC"));
    Ok(0)
}

fn read_context(
    server: &str,
    host: &str,
    now: DateTime<Utc>,
    timeout_seconds: u64,
) -> ContextState {
    let mut state = ContextState::default();
    if let Ok(Some(event)) = get_latest_event(
        server,
        &format!("aw-worktime-sessions_{host}"),
        timeout_seconds,
    ) {
        if let Some(ts) = event_timestamp(&event) {
            let age = (now - ts).num_seconds();
            let active = event
                .pointer("/data/active")
                .and_then(Value::as_bool)
                .unwrap_or(false);
            if (0..900).contains(&age) && !active {
                state.host_inactive = true;
            }
        }
    }
    if let Ok(Some(event)) = get_latest_event(
        server,
        &format!("aw-rus-collector-guard_{host}"),
        timeout_seconds,
    ) {
        if let Some(ts) = event_timestamp(&event) {
            let age = (now - ts).num_seconds();
            let status = event
                .pointer("/data/status")
                .and_then(Value::as_str)
                .unwrap_or("");
            let problems = event
                .pointer("/data/problems")
                .and_then(Value::as_array)
                .map(Vec::len)
                .unwrap_or(0);
            if (0..300).contains(&age) && status == "ok" && problems == 0 {
                state.guard_healthy = true;
            }
        }
    }
    state
}

fn bucket_event(
    server: &str,
    bucket: &str,
    buckets_index: Option<&Value>,
    with_event_ids: bool,
    timeout_seconds: u64,
) -> Option<Value> {
    if with_event_ids {
        get_latest_event(server, bucket, timeout_seconds)
            .ok()
            .flatten()
            .or_else(|| metadata_event(buckets_index, bucket))
    } else {
        metadata_event(buckets_index, bucket)
    }
}

fn render_bucket(
    bucket: &str,
    event: Option<&Value>,
    now: DateTime<Utc>,
    context: &ContextState,
) -> (String, String, String, BucketStatus) {
    let Some(event) = event else {
        return (
            "0".to_string(),
            "no events".to_string(),
            "none".to_string(),
            classify_bucket_no_events(bucket, context),
        );
    };
    let last_id = event
        .get("id")
        .map(json_value_to_string)
        .unwrap_or_else(|| "0".to_string());
    let Some(ts_raw) = event.get("timestamp").and_then(Value::as_str) else {
        return (
            last_id,
            "no events".to_string(),
            "none".to_string(),
            classify_bucket_no_events(bucket, context),
        );
    };
    let Some(ts) = parse_ts(ts_raw) else {
        return (
            last_id,
            ts_raw.to_string(),
            "unknown".to_string(),
            BucketStatus::Unknown,
        );
    };
    let age = (now - ts).num_seconds().max(0);
    (
        last_id,
        ts_raw.to_string(),
        format_age(age),
        classify_bucket_age(bucket, age, context),
    )
}

fn classify_bucket_age(bucket: &str, age_sec: i64, context: &ContextState) -> BucketStatus {
    match bucket {
        "aw-watcher-window" if context.host_inactive => return BucketStatus::Inactive,
        "aw-dlp-endpoint-signals" if context.host_inactive && context.guard_healthy => {
            return BucketStatus::Inactive;
        }
        _ => {}
    }

    match bucket {
        "aw-dlp-incidents" | "aw-dlp-review" | "aw-dlp-rules" | "aw-session-events" => {
            if age_sec < 86_400 {
                BucketStatus::Fresh
            } else {
                BucketStatus::EventDriven
            }
        }
        _ if age_sec < 3_600 => BucketStatus::Fresh,
        _ if age_sec < 86_400 => BucketStatus::Stale,
        _ => BucketStatus::Dead,
    }
}

fn classify_bucket_no_events(bucket: &str, context: &ContextState) -> BucketStatus {
    match bucket {
        "aw-watcher-window" if context.host_inactive => BucketStatus::Inactive,
        "aw-dlp-endpoint-signals" if context.host_inactive && context.guard_healthy => {
            BucketStatus::Inactive
        }
        "aw-dlp-incidents" | "aw-dlp-review" | "aw-dlp-rules" | "aw-session-events" => {
            BucketStatus::EventDriven
        }
        _ => BucketStatus::Empty,
    }
}

fn render_status(colors: &Colors, status: BucketStatus) -> String {
    match status {
        BucketStatus::Fresh => colors.paint(colors.green, "FRESH"),
        BucketStatus::Stale => colors.paint(colors.yellow, "STALE"),
        BucketStatus::Dead => colors.paint(colors.red, "DEAD"),
        BucketStatus::Empty => colors.paint(colors.red, "EMPTY"),
        BucketStatus::EventDriven => colors.paint(colors.cyan, "EVENT-DRIVEN"),
        BucketStatus::Inactive => colors.paint(colors.cyan, "INACTIVE"),
        BucketStatus::Unknown => colors.paint(colors.red, "?"),
    }
}

fn get_latest_event(server: &str, bucket: &str, timeout_seconds: u64) -> Result<Option<Value>> {
    let url = format!("{server}/api/0/buckets/{bucket}/events?limit=1");
    let client = Client::builder()
        .timeout(Duration::from_secs(timeout_seconds.max(1)))
        .no_proxy()
        .pool_max_idle_per_host(0)
        .build()
        .context("build timed HTTP client")?;
    let value = client
        .get(&url)
        .header("Connection", "close")
        .send()
        .with_context(|| format!("GET {url}"))?
        .error_for_status()
        .with_context(|| format!("GET {url} status"))?
        .json::<Value>()
        .with_context(|| format!("decode JSON from {url}"))?;
    Ok(value.as_array().and_then(|items| items.first()).cloned())
}

fn metadata_event(buckets_index: Option<&Value>, bucket: &str) -> Option<Value> {
    let bucket_info = buckets_index?.get(bucket)?;
    let timestamp = bucket_info
        .pointer("/metadata/end")
        .or_else(|| bucket_info.get("end"))
        .and_then(Value::as_str)?;
    Some(serde_json::json!({
        "id": 0,
        "timestamp": timestamp,
        "data": {},
        "_source": "bucket_metadata",
    }))
}

fn get_json(client: &Client, url: &str, _timeout_seconds: u64) -> Result<Value> {
    client
        .get(url)
        .send()
        .with_context(|| format!("GET {url}"))?
        .error_for_status()
        .with_context(|| format!("GET {url} status"))?
        .json::<Value>()
        .with_context(|| format!("decode JSON from {url}"))
}

fn check_cors(_client: &Client, server: &str) -> u16 {
    let origin = "http://192.0.2.13:5600";
    let first = curl_status(&format!("{server}/api/0/settings/"), origin);
    if first == 200 {
        return first;
    }
    if !server.contains("127.0.0.1") && !server.contains("localhost") {
        let fallback = curl_status("http://127.0.0.1:5600/api/0/settings/", origin);
        if fallback != 0 {
            return fallback;
        }
    }
    first
}

fn curl_status(url: &str, origin: &str) -> u16 {
    let origin_header = format!("Origin: {origin}");
    let args = [
        "-s",
        "--connect-timeout",
        "3",
        "--max-time",
        "5",
        "-o",
        "/dev/null",
        "-w",
        "%{http_code}",
        "-H",
        origin_header.as_str(),
        url,
    ];
    let output = Command::new("/usr/bin/curl")
        .args(args)
        .output()
        .or_else(|_| Command::new("curl").args(args).output());
    let Ok(output) = output else {
        return 0;
    };
    if !output.status.success() {
        return 0;
    }
    String::from_utf8_lossy(&output.stdout)
        .trim()
        .parse::<u16>()
        .unwrap_or(0)
}

fn event_timestamp(event: &Value) -> Option<DateTime<Utc>> {
    event
        .get("timestamp")
        .and_then(Value::as_str)
        .and_then(parse_ts)
}

fn parse_ts(value: &str) -> Option<DateTime<Utc>> {
    DateTime::parse_from_rfc3339(&value.replace('Z', "+00:00"))
        .map(|ts| ts.with_timezone(&Utc))
        .ok()
}

fn format_age(age_sec: i64) -> String {
    if age_sec < 3_600 {
        format!("{}m ago", age_sec / 60)
    } else if age_sec < 86_400 {
        format!("{}h ago", age_sec / 3_600)
    } else {
        format!("{}d ago", age_sec / 86_400)
    }
}

fn json_value_to_string(value: &Value) -> String {
    match value {
        Value::String(value) => value.clone(),
        other => other.to_string(),
    }
}

fn env_nonempty(name: &str) -> Option<String> {
    std::env::var(name)
        .ok()
        .map(|value| value.trim().to_string())
        .filter(|value| !value.is_empty())
}

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

    #[test]
    fn event_driven_buckets_do_not_become_dead_when_old() {
        let context = ContextState::default();
        assert_eq!(
            classify_bucket_age("aw-dlp-incidents", 100_000, &context),
            BucketStatus::EventDriven
        );
    }

    #[test]
    fn inactive_host_suppresses_window_stale() {
        let context = ContextState {
            host_inactive: true,
            guard_healthy: true,
        };
        assert_eq!(
            classify_bucket_age("aw-watcher-window", 100_000, &context),
            BucketStatus::Inactive
        );
        assert_eq!(
            classify_bucket_no_events("aw-dlp-endpoint-signals", &context),
            BucketStatus::Inactive
        );
    }

    #[test]
    fn formats_age_like_legacy_script() {
        assert_eq!(format_age(59), "0m ago");
        assert_eq!(format_age(3_600), "1h ago");
        assert_eq!(format_age(86_400), "1d ago");
    }

    #[test]
    fn metadata_only_bucket_event_skips_missing_deep_event_read() {
        let index = serde_json::json!({
            "aw-watcher-window_HOST-EXAMPLE": {
                "metadata": {
                    "end": "2026-06-02T00:00:00Z"
                }
            }
        });
        let event = bucket_event(
            "http://127.0.0.1:1",
            "aw-watcher-window_HOST-EXAMPLE",
            Some(&index),
            false,
            1,
        )
        .expect("metadata event");
        assert_eq!(
            event.get("timestamp").and_then(Value::as_str),
            Some("2026-06-02T00:00:00Z")
        );
        assert_eq!(event.get("id").and_then(Value::as_i64), Some(0));
    }
}
