use anyhow::{bail, Result};
use chrono::Utc;
use regex::Regex;

/// Parses a relative duration string like "1h", "30m", "7d" into milliseconds.
///
/// Supported formats:
///   - Short: "30s", "10m", "1h", "7d", "1w"
///   - Long: "5min", "5minutes", "2hours", "3days", "1week"
///   - With spaces: "5 minutes", "2 hours"
///   - Leading minus is stripped: "-5m" → same as "5m"
fn parse_relative_duration_millis(input: &str) -> Result<i64> {
    let stripped = input.trim_start_matches('-').trim();

    let re = Regex::new(
        r"(?i)^(\d+)\s*(s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours|d|day|days|w|week|weeks)$",
    )
    .unwrap();

    if let Some(caps) = re.captures(stripped) {
        let num: i64 = caps[1].parse()?;
        let unit = caps[2].to_lowercase();
        let seconds = match unit.as_str() {
            "s" | "sec" | "secs" | "second" | "seconds" => num,
            "m" | "min" | "mins" | "minute" | "minutes" => num * 60,
            "h" | "hr" | "hrs" | "hour" | "hours" => num * 3600,
            "d" | "day" | "days" => num * 86400,
            "w" | "week" | "weeks" => num * 7 * 86400,
            _ => bail!("unknown time unit: {}", unit),
        };
        return Ok(seconds * 1000);
    }

    bail!("unable to parse duration: {input:?}")
}

/// Parses a time string into Unix milliseconds.
///
/// Supported formats:
///   - "now" (case-insensitive)
///   - Relative short: "1h", "30m", "7d", "5s", "1w"
///   - Relative long: "5min", "5mins", "5minute", "5minutes", "2hr", "2hours", "3days", "1week"
///   - With spaces: "5 minutes", "2 hours"
///   - With leading minus: "-5m", "-2h"
///   - Unix timestamp in seconds (10 digits or fewer) or milliseconds
///   - RFC3339: "2024-01-01T00:00:00Z"
///
/// All relative times are interpreted as "ago from now".
/// Returns second-aligned milliseconds (Unix seconds * 1000) to match Go behavior.
pub fn parse_time_to_unix_millis(input: &str) -> Result<i64> {
    let input = input.trim();

    if input.eq_ignore_ascii_case("now") {
        return Ok(now_millis());
    }

    // Unix timestamp (all digits). Treat 10-digit values as seconds to match
    // CLI examples and common shell usage like `date +%s`; preserve longer
    // values as milliseconds.
    if !input.is_empty() && input.chars().all(|c| c.is_ascii_digit()) {
        let ts: i64 = input.parse()?;
        return if input.len() <= 10 {
            Ok(ts * 1000)
        } else {
            Ok(ts)
        };
    }

    // RFC3339 timestamp
    if input.contains('T') {
        let dt = chrono::DateTime::parse_from_rfc3339(input)?;
        return Ok(dt.timestamp() * 1000);
    }

    // Relative time
    let millis = parse_relative_duration_millis(input).map_err(|_| {
        anyhow::anyhow!(
            "unable to parse time: {input:?}\n\
             Expected: now, 1h, 30m, 7d, 5minutes, RFC3339, or Unix timestamp"
        )
    })?;
    Ok(now_millis() - millis)
}

/// Convenience: parse to Unix seconds.
pub fn parse_time_to_unix(input: &str) -> Result<i64> {
    Ok(parse_time_to_unix_millis(input)? / 1000)
}

/// Parses a time string into a `chrono::DateTime<Utc>`.
///
/// Returns an `anyhow` error if the input cannot be parsed or if the resulting
/// timestamp falls outside chrono's representable range.
pub fn parse_time_to_datetime(input: &str) -> Result<chrono::DateTime<Utc>> {
    let ms = parse_time_to_unix_millis(input)?;
    chrono::DateTime::from_timestamp_millis(ms)
        .ok_or_else(|| anyhow::anyhow!("timestamp out of valid range: {input:?}"))
}

/// Parses a human-readable duration string into milliseconds.
///
/// Unlike `parse_time_to_unix_millis`, this does **not** subtract from the
/// current time — it returns the raw duration value in milliseconds.
///
/// Supported formats:
///   - "now" → 0
///   - Short: "30s", "10m", "1h", "7d", "1w"
///   - Long: "5min", "5minutes", "2hours", "3days", "1week"
///   - With spaces: "5 minutes", "2 hours"
///   - With leading minus (stripped): "-5m" → same as "5m"
pub fn parse_duration_to_millis(input: &str) -> Result<i64> {
    let input = input.trim();

    if input.eq_ignore_ascii_case("now") {
        return Ok(0);
    }

    parse_relative_duration_millis(input).map_err(|_| {
        anyhow::anyhow!(
            "unable to parse duration: {input:?}\n\
             Expected: now, 1h, 30m, 7d, 5minutes, etc."
        )
    })
}

pub fn now_millis() -> i64 {
    Utc::now().timestamp() * 1000
}

/// Read a JSON file and deserialize into the specified type.
/// Used by create/update commands that accept `--file` input.
pub fn read_json_file<T: serde::de::DeserializeOwned>(path: &str) -> Result<T> {
    let contents = std::fs::read_to_string(path)
        .map_err(|e| anyhow::anyhow!("failed to read file {path:?}: {e}"))?;
    serde_json::from_str(&contents)
        .map_err(|e| anyhow::anyhow!("failed to parse JSON from {path:?}: {e}"))
}

/// Parses a UUID string, returning a descriptive error if invalid.
pub fn parse_uuid(id: &str, label: &str) -> anyhow::Result<uuid::Uuid> {
    uuid::Uuid::parse_str(id).map_err(|e| anyhow::anyhow!("invalid {label} UUID '{id}': {e}"))
}

/// Parse a compute string like "count", "avg(@duration)", "percentile(@duration, 99)"
/// into a (function_name, Option<metric>) pair as raw strings.
pub(crate) fn parse_compute_raw(input: &str) -> Result<(String, Option<String>)> {
    let input = input.trim();
    if input.is_empty() {
        bail!("--compute is required");
    }

    // Simple aggregations without a metric
    if input == "count" {
        return Ok(("count".into(), None));
    }

    // func(@field) pattern
    if let Some(paren) = input.find('(') {
        let func = &input[..paren];
        let rest = input[paren + 1..].trim_end_matches(')').trim();

        // Handle percentile(@field, N)
        if func == "percentile" {
            let parts: Vec<&str> = rest.splitn(2, ',').collect();
            if parts.len() != 2 {
                bail!("percentile requires field and value: percentile(@duration, 99)");
            }
            let metric = parts[0].trim().to_string();
            let pct: u32 = parts[1]
                .trim()
                .parse()
                .map_err(|_| anyhow::anyhow!("invalid percentile value: {}", parts[1].trim()))?;
            let agg_name = match pct {
                75 => "pc75",
                90 => "pc90",
                95 => "pc95",
                98 => "pc98",
                99 => "pc99",
                _ => bail!("unsupported percentile: {pct} (supported: 75, 90, 95, 98, 99)"),
            };
            return Ok((agg_name.into(), Some(metric)));
        }

        let metric = rest.to_string();
        let agg_name = match func {
            "avg" | "sum" | "min" | "max" | "median" | "cardinality" => func.to_string(),
            "count" => bail!("count does not accept a field argument; use just 'count'"),
            _ => bail!("unknown aggregation function: {func}"),
        };
        return Ok((agg_name, Some(metric)));
    }

    bail!(
        "invalid --compute format: {input:?}\n\
         Expected: count, avg(@duration), sum(@duration), percentile(@duration, 99), etc."
    )
}

/// Percent-encode a string for safe use in URL paths and query values.
///
/// Unreserved characters (RFC 3986 §2.3) — `A-Z a-z 0-9 - _ . ~` — are
/// passed through; every other byte is encoded as `%XX`.  Spaces become
/// `%20`, not `+`.
///
/// The implementation is intentionally hand-rolled rather than pulling in the
/// `percent-encoding` crate: that crate is only a transitive dependency (via
/// `url`/`reqwest`) and its `AsciiSet` API requires more boilerplate than this
/// 8-line loop for the single RFC 3986 unreserved-character set we need.
pub fn percent_encode(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    for &b in s.as_bytes() {
        match b {
            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
                out.push(b as char)
            }
            _ => out.push_str(&format!("%{b:02X}")),
        }
    }
    out
}

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

    #[test]
    fn test_now() {
        let ms = parse_time_to_unix_millis("now").unwrap();
        let diff = (Utc::now().timestamp() * 1000 - ms).abs();
        assert!(diff < 2000, "now should be within 2s: diff={diff}ms");
    }

    #[test]
    fn test_now_case_insensitive() {
        assert!(parse_time_to_unix_millis("NOW").is_ok());
        assert!(parse_time_to_unix_millis("Now").is_ok());
    }

    #[test]
    fn test_relative_short() {
        let ms = parse_time_to_unix_millis("1h").unwrap();
        let expected = (Utc::now().timestamp() - 3600) * 1000;
        assert!((ms - expected).abs() < 2000);
    }

    #[test]
    fn test_relative_long() {
        let ms = parse_time_to_unix_millis("5minutes").unwrap();
        let expected = (Utc::now().timestamp() - 300) * 1000;
        assert!((ms - expected).abs() < 2000);
    }

    #[test]
    fn test_relative_with_spaces() {
        let ms = parse_time_to_unix_millis("5 minutes").unwrap();
        let expected = (Utc::now().timestamp() - 300) * 1000;
        assert!((ms - expected).abs() < 2000);
    }

    #[test]
    fn test_relative_with_minus() {
        let ms = parse_time_to_unix_millis("-30m").unwrap();
        let expected = (Utc::now().timestamp() - 1800) * 1000;
        assert!((ms - expected).abs() < 2000);
    }

    #[test]
    fn test_unix_timestamp() {
        let ms = parse_time_to_unix_millis("1700000000000").unwrap();
        assert_eq!(ms, 1700000000000);
    }

    #[test]
    fn test_unix_timestamp_seconds_are_expanded_to_millis() {
        let ms = parse_time_to_unix_millis("1707048000").unwrap();
        assert_eq!(ms, 1707048000000);
    }

    #[test]
    fn test_unix_timestamp_milliseconds_are_preserved() {
        let ms = parse_time_to_unix_millis("1707048000000").unwrap();
        assert_eq!(ms, 1707048000000);
    }

    #[test]
    fn test_rfc3339() {
        let ms = parse_time_to_unix_millis("2024-01-01T00:00:00Z").unwrap();
        assert_eq!(ms, 1704067200000);
    }

    #[test]
    fn test_invalid() {
        assert!(parse_time_to_unix_millis("invalid").is_err());
        assert!(parse_time_to_unix_millis("").is_err());
    }

    #[test]
    fn test_parse_time_to_datetime_relative() {
        let dt = parse_time_to_datetime("1h").unwrap();
        let expected = Utc::now().timestamp() - 3600;
        assert!((dt.timestamp() - expected).abs() < 2);
    }

    #[test]
    fn test_parse_time_to_datetime_long_form() {
        let dt = parse_time_to_datetime("2hours").unwrap();
        let expected = Utc::now().timestamp() - 7200;
        assert!((dt.timestamp() - expected).abs() < 2);
    }

    #[test]
    fn test_parse_time_to_datetime_unix_millis() {
        let dt = parse_time_to_datetime("1700000000000").unwrap();
        assert_eq!(dt.timestamp_millis(), 1700000000000);
    }

    #[test]
    fn test_parse_time_to_datetime_rfc3339() {
        let dt = parse_time_to_datetime("2024-01-01T00:00:00Z").unwrap();
        assert_eq!(dt.timestamp_millis(), 1704067200000);
    }

    #[test]
    fn test_parse_time_to_datetime_invalid_input() {
        let err = parse_time_to_datetime("not-a-time").unwrap_err();
        assert!(err.to_string().contains("unable to parse time"));
    }

    #[test]
    fn test_parse_time_to_datetime_empty() {
        assert!(parse_time_to_datetime("").is_err());
    }

    #[test]
    fn test_parse_time_to_unix_returns_seconds() {
        let secs = parse_time_to_unix("1700000000000").unwrap();
        assert_eq!(secs, 1700000000);
    }

    #[test]
    fn test_parse_time_to_unix_now() {
        let secs = parse_time_to_unix("now").unwrap();
        let expected = Utc::now().timestamp();
        assert!((secs - expected).abs() < 2);
    }

    #[test]
    fn test_relative_days() {
        let ms = parse_time_to_unix_millis("7d").unwrap();
        let expected = (Utc::now().timestamp() - 7 * 86400) * 1000;
        assert!((ms - expected).abs() < 2000);
    }

    #[test]
    fn test_relative_weeks() {
        let ms = parse_time_to_unix_millis("1w").unwrap();
        let expected = (Utc::now().timestamp() - 7 * 86400) * 1000;
        assert!((ms - expected).abs() < 2000);
    }

    #[test]
    fn test_relative_seconds() {
        let ms = parse_time_to_unix_millis("30s").unwrap();
        let expected = (Utc::now().timestamp() - 30) * 1000;
        assert!((ms - expected).abs() < 2000);
    }

    #[test]
    fn test_relative_long_hours() {
        let ms = parse_time_to_unix_millis("2hours").unwrap();
        let expected = (Utc::now().timestamp() - 7200) * 1000;
        assert!((ms - expected).abs() < 2000);
    }

    #[test]
    fn test_relative_long_days() {
        let ms = parse_time_to_unix_millis("3days").unwrap();
        let expected = (Utc::now().timestamp() - 3 * 86400) * 1000;
        assert!((ms - expected).abs() < 2000);
    }

    #[test]
    fn test_relative_long_weeks() {
        let ms = parse_time_to_unix_millis("1week").unwrap();
        let expected = (Utc::now().timestamp() - 7 * 86400) * 1000;
        assert!((ms - expected).abs() < 2000);
    }

    #[test]
    fn test_percent_encode_unreserved_passthrough() {
        // RFC 3986 §2.3 unreserved characters must not be encoded.
        assert_eq!(percent_encode("abc"), "abc");
        assert_eq!(percent_encode("foo.bar"), "foo.bar");
        assert_eq!(percent_encode("foo-bar_baz"), "foo-bar_baz");
        assert_eq!(percent_encode("UPPER"), "UPPER");
        assert_eq!(percent_encode("123"), "123");
        assert_eq!(percent_encode("foo~bar"), "foo~bar"); // tilde is unreserved
    }

    #[test]
    fn test_percent_encode_special_chars() {
        assert_eq!(percent_encode("env:prod"), "env%3Aprod");
        assert_eq!(percent_encode("k8s cluster"), "k8s%20cluster");
        assert_eq!(percent_encode("a&b=c"), "a%26b%3Dc");
        assert_eq!(percent_encode("path/value"), "path%2Fvalue");
    }

    #[test]
    fn test_percent_encode_empty() {
        assert_eq!(percent_encode(""), "");
    }

    #[test]
    fn test_percent_encode_multibyte_utf8() {
        // Multi-byte UTF-8 characters must be encoded byte-by-byte per RFC 3986.
        // "é" is 0xC3 0xA9 in UTF-8.
        assert_eq!(percent_encode("café"), "caf%C3%A9");
    }

    #[test]
    fn test_read_json_file_missing() {
        let result: Result<serde_json::Value> = read_json_file("/tmp/__pup_nonexistent__.json");
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("failed to read"));
    }

    #[test]
    fn test_read_json_file_invalid_json() {
        let path = std::env::temp_dir().join("__pup_test_invalid__.json");
        std::fs::write(&path, "not json").unwrap();
        let result: Result<serde_json::Value> = read_json_file(path.to_str().unwrap());
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("failed to parse"));
        std::fs::remove_file(&path).ok();
    }

    #[test]
    fn test_read_json_file_valid() {
        let path = std::env::temp_dir().join("__pup_test_valid__.json");
        std::fs::write(&path, r#"{"name": "test"}"#).unwrap();
        let result: Result<serde_json::Value> = read_json_file(path.to_str().unwrap());
        assert!(result.is_ok());
        assert_eq!(result.unwrap()["name"], "test");
        std::fs::remove_file(&path).ok();
    }

    #[test]
    fn test_duration_10m() {
        assert_eq!(parse_duration_to_millis("10m").unwrap(), 600_000);
    }

    #[test]
    fn test_duration_1h() {
        assert_eq!(parse_duration_to_millis("1h").unwrap(), 3_600_000);
    }

    #[test]
    fn test_duration_30s() {
        assert_eq!(parse_duration_to_millis("30s").unwrap(), 30_000);
    }

    #[test]
    fn test_duration_7d() {
        assert_eq!(parse_duration_to_millis("7d").unwrap(), 604_800_000);
    }

    #[test]
    fn test_duration_5minutes() {
        assert_eq!(parse_duration_to_millis("5minutes").unwrap(), 300_000);
    }

    #[test]
    fn test_duration_now() {
        assert_eq!(parse_duration_to_millis("now").unwrap(), 0);
    }

    #[test]
    fn test_parse_compute_count() {
        let (aggregation, metric) = parse_compute_raw("count").unwrap();
        assert_eq!(aggregation, "count");
        assert!(metric.is_none());
    }

    #[test]
    fn test_parse_compute_avg() {
        let (aggregation, metric) = parse_compute_raw("avg(@duration)").unwrap();
        assert_eq!(aggregation, "avg");
        assert_eq!(metric.unwrap(), "@duration");
    }

    #[test]
    fn test_parse_compute_sum() {
        let (aggregation, metric) = parse_compute_raw("sum(@duration)").unwrap();
        assert_eq!(aggregation, "sum");
        assert_eq!(metric.unwrap(), "@duration");
    }

    #[test]
    fn test_parse_compute_min() {
        let (aggregation, metric) = parse_compute_raw("min(@duration)").unwrap();
        assert_eq!(aggregation, "min");
        assert_eq!(metric.unwrap(), "@duration");
    }

    #[test]
    fn test_parse_compute_max() {
        let (aggregation, metric) = parse_compute_raw("max(@duration)").unwrap();
        assert_eq!(aggregation, "max");
        assert_eq!(metric.unwrap(), "@duration");
    }

    #[test]
    fn test_parse_compute_median() {
        let (aggregation, metric) = parse_compute_raw("median(@duration)").unwrap();
        assert_eq!(aggregation, "median");
        assert_eq!(metric.unwrap(), "@duration");
    }

    #[test]
    fn test_parse_compute_cardinality() {
        let (aggregation, metric) = parse_compute_raw("cardinality(@usr.id)").unwrap();
        assert_eq!(aggregation, "cardinality");
        assert_eq!(metric.unwrap(), "@usr.id");
    }

    #[test]
    fn test_parse_compute_percentile_99() {
        let (aggregation, metric) = parse_compute_raw("percentile(@duration, 99)").unwrap();
        assert_eq!(aggregation, "pc99");
        assert_eq!(metric.unwrap(), "@duration");
    }

    #[test]
    fn test_parse_compute_percentile_95() {
        let (aggregation, metric) = parse_compute_raw("percentile(@duration, 95)").unwrap();
        assert_eq!(aggregation, "pc95");
        assert_eq!(metric.unwrap(), "@duration");
    }

    #[test]
    fn test_parse_compute_percentile_90() {
        let (aggregation, metric) = parse_compute_raw("percentile(@duration, 90)").unwrap();
        assert_eq!(aggregation, "pc90");
        assert_eq!(metric.unwrap(), "@duration");
    }

    #[test]
    fn test_parse_compute_empty() {
        assert!(parse_compute_raw("").is_err());
    }

    #[test]
    fn test_parse_compute_invalid() {
        assert!(parse_compute_raw("invalid").is_err());
    }

    #[test]
    fn test_parse_compute_unknown_function() {
        assert!(parse_compute_raw("foo(@bar)").is_err());
    }

    #[test]
    fn test_parse_compute_unsupported_percentile() {
        assert!(parse_compute_raw("percentile(@duration, 42)").is_err());
    }

    #[test]
    fn test_parse_compute_percentile_missing_value() {
        assert!(parse_compute_raw("percentile(@duration)").is_err());
    }

    #[test]
    fn test_parse_compute_rejects_invalid_count_metric() {
        let err = parse_compute_raw("count(@duration)").unwrap_err();
        assert!(err.to_string().contains("does not accept a field"));
    }
}
