use std::collections::BTreeMap;

use chrono::{
    DateTime, Datelike, LocalResult, NaiveDate, NaiveDateTime, Offset, TimeZone, Timelike, Utc,
};
use chrono_tz::Tz;

use crate::stdlib::macros::{harn_builtin, VmBuiltinDef};
use crate::value::{VmError, VmValue};
use crate::vm::Vm;

const DEFAULT_FORMAT: &str = "%Y-%m-%d %H:%M:%S";

pub(crate) fn register_datetime_builtins(vm: &mut Vm) {
    for def in MODULE_BUILTINS {
        vm.register_builtin_def(def);
    }
}

#[harn_builtin(sig = "date_now() -> dict", category = "datetime")]
fn date_now_impl(_args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
    let now = Utc::now();
    let mut result = utc_datetime_dict(now);
    result.insert(
        "timestamp".to_string(),
        VmValue::Float(now.timestamp_millis() as f64 / 1000.0),
    );
    result.insert(
        "iso8601".to_string(),
        VmValue::String(std::sync::Arc::from(
            now.to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
        )),
    );
    Ok(VmValue::Dict(std::sync::Arc::new(result)))
}

#[harn_builtin(sig = "date_now_iso() -> string", category = "datetime")]
fn date_now_iso_impl(_args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
    Ok(VmValue::String(std::sync::Arc::from(
        Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
    )))
}

#[harn_builtin(
    sig = "date_format(timestamp: int | float | dict, format?: string, timezone?: string) -> string",
    category = "datetime"
)]
fn date_format_impl(args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
    let dt = datetime_from_arg(args.first(), "date_format")?;
    let fmt = args
        .get(1)
        .map(|a| a.display())
        .unwrap_or_else(|| DEFAULT_FORMAT.to_string());
    if let Some(tz_arg) = args.get(2) {
        let tz = parse_timezone(&tz_arg.display(), "date_format")?;
        return Ok(VmValue::String(std::sync::Arc::from(
            dt.with_timezone(&tz).format(&fmt).to_string(),
        )));
    }
    Ok(VmValue::String(std::sync::Arc::from(
        dt.format(&fmt).to_string(),
    )))
}

#[harn_builtin(
    sig = "date_parse(text: string?) -> int | float",
    category = "datetime"
)]
fn date_parse_impl(args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
    let input = args.first().map(|a| a.display()).unwrap_or_default();
    let dt = parse_datetime_auto(&input)?;
    Ok(timestamp_value(dt))
}

#[harn_builtin(
    sig = "date_in_zone(timestamp: int | float | dict, timezone: string) -> dict",
    category = "datetime"
)]
fn date_in_zone_impl(args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
    let dt = datetime_from_arg(args.first(), "date_in_zone")?;
    let tz_arg = require_arg(args, 1, "date_in_zone", "timezone")?;
    let tz_name = tz_arg.display();
    let tz = parse_timezone(&tz_name, "date_in_zone")?;
    let local = dt.with_timezone(&tz);
    let mut result = zoned_datetime_dict(local);
    result.insert(
        "zone".to_string(),
        VmValue::String(std::sync::Arc::from(tz_name)),
    );
    Ok(VmValue::Dict(std::sync::Arc::new(result)))
}

#[harn_builtin(
    sig = "date_to_zone(timestamp: int | float | dict, timezone: string) -> string",
    category = "datetime"
)]
fn date_to_zone_impl(args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
    let dt = datetime_from_arg(args.first(), "date_to_zone")?;
    let tz_arg = require_arg(args, 1, "date_to_zone", "timezone")?;
    let tz = parse_timezone(&tz_arg.display(), "date_to_zone")?;
    Ok(VmValue::String(std::sync::Arc::from(
        dt.with_timezone(&tz).to_rfc3339(),
    )))
}

#[harn_builtin(
    sig = "date_from_components(components: dict, timezone?: string) -> int | float",
    category = "datetime"
)]
fn date_from_components_impl(args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
    let components = require_dict(args.first(), "date_from_components")?;
    let tz = match args.get(1) {
        Some(VmValue::Nil) | None => chrono_tz::UTC,
        Some(value) => parse_timezone(&value.display(), "date_from_components")?,
    };
    let dt = datetime_from_components(components, tz)?;
    Ok(timestamp_value(dt.with_timezone(&Utc)))
}

#[harn_builtin(
    sig = "duration_ms(count: int | float) -> duration",
    category = "datetime"
)]
fn duration_ms_impl(args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
    duration_from_number(args.first(), 1, "duration_ms").map(VmValue::Duration)
}

#[harn_builtin(
    sig = "duration_seconds(count: int | float) -> duration",
    category = "datetime"
)]
fn duration_seconds_impl(args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
    duration_from_number(args.first(), 1_000, "duration_seconds").map(VmValue::Duration)
}

#[harn_builtin(
    sig = "duration_minutes(count: int | float) -> duration",
    category = "datetime"
)]
fn duration_minutes_impl(args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
    duration_from_number(args.first(), 60_000, "duration_minutes").map(VmValue::Duration)
}

#[harn_builtin(
    sig = "duration_hours(count: int | float) -> duration",
    category = "datetime"
)]
fn duration_hours_impl(args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
    duration_from_number(args.first(), 3_600_000, "duration_hours").map(VmValue::Duration)
}

#[harn_builtin(
    sig = "duration_days(count: int | float) -> duration",
    category = "datetime"
)]
fn duration_days_impl(args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
    duration_from_number(args.first(), 86_400_000, "duration_days").map(VmValue::Duration)
}

#[harn_builtin(
    sig = "date_add(timestamp: int | float | dict, duration: duration) -> int | float",
    category = "datetime"
)]
fn date_add_impl(args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
    let millis = timestamp_millis_from_arg(args.first(), "date_add")?;
    let duration = require_duration(args.get(1), "date_add")?;
    timestamp_millis_value(
        millis
            .checked_add(duration as i128)
            .ok_or_else(|| vm_error("date_add: timestamp overflow"))?,
    )
}

#[harn_builtin(
    sig = "date_diff(a: int | float | dict, b: int | float | dict) -> duration",
    category = "datetime"
)]
fn date_diff_impl(args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
    let a = timestamp_millis_from_arg(args.first(), "date_diff")?;
    let b = timestamp_millis_from_arg(args.get(1), "date_diff")?;
    let diff = a
        .checked_sub(b)
        .ok_or_else(|| vm_error("date_diff: duration overflow"))?;
    let diff = i64::try_from(diff).map_err(|_| vm_error("date_diff: duration overflow"))?;
    Ok(VmValue::Duration(diff))
}

#[harn_builtin(
    sig = "duration_to_seconds(duration: duration) -> int",
    category = "datetime"
)]
fn duration_to_seconds_impl(args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
    let duration = require_duration(args.first(), "duration_to_seconds")?;
    Ok(VmValue::Int(duration / 1_000))
}

#[harn_builtin(
    sig = "duration_to_human(duration: duration) -> string",
    category = "datetime"
)]
fn duration_to_human_impl(args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
    let duration = require_duration(args.first(), "duration_to_human")?;
    Ok(VmValue::String(std::sync::Arc::from(
        format_duration_human(duration),
    )))
}

#[harn_builtin(
    sig = "weekday_name(timestamp: int | float | dict, timezone?: string) -> string",
    category = "datetime"
)]
fn weekday_name_impl(args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
    let dt = datetime_from_arg(args.first(), "weekday_name")?;
    let name = match args.get(1) {
        Some(VmValue::Nil) | None => dt.format("%A").to_string(),
        Some(value) => {
            let tz = parse_timezone(&value.display(), "weekday_name")?;
            dt.with_timezone(&tz).format("%A").to_string()
        }
    };
    Ok(VmValue::String(std::sync::Arc::from(name)))
}

#[harn_builtin(
    sig = "month_name(timestamp: int | float | dict, timezone?: string) -> string",
    category = "datetime"
)]
fn month_name_impl(args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
    let dt = datetime_from_arg(args.first(), "month_name")?;
    let name = match args.get(1) {
        Some(VmValue::Nil) | None => dt.format("%B").to_string(),
        Some(value) => {
            let tz = parse_timezone(&value.display(), "month_name")?;
            dt.with_timezone(&tz).format("%B").to_string()
        }
    };
    Ok(VmValue::String(std::sync::Arc::from(name)))
}

pub(crate) const MODULE_BUILTINS: &[&VmBuiltinDef] = &[
    &DATE_NOW_IMPL_DEF,
    &DATE_NOW_ISO_IMPL_DEF,
    &DATE_FORMAT_IMPL_DEF,
    &DATE_PARSE_IMPL_DEF,
    &DATE_IN_ZONE_IMPL_DEF,
    &DATE_TO_ZONE_IMPL_DEF,
    &DATE_FROM_COMPONENTS_IMPL_DEF,
    &DURATION_MS_IMPL_DEF,
    &DURATION_SECONDS_IMPL_DEF,
    &DURATION_MINUTES_IMPL_DEF,
    &DURATION_HOURS_IMPL_DEF,
    &DURATION_DAYS_IMPL_DEF,
    &DATE_ADD_IMPL_DEF,
    &DATE_DIFF_IMPL_DEF,
    &DURATION_TO_SECONDS_IMPL_DEF,
    &DURATION_TO_HUMAN_IMPL_DEF,
    &WEEKDAY_NAME_IMPL_DEF,
    &MONTH_NAME_IMPL_DEF,
];

fn require_arg<'a>(
    args: &'a [VmValue],
    index: usize,
    builtin: &str,
    label: &str,
) -> Result<&'a VmValue, VmError> {
    args.get(index)
        .ok_or_else(|| vm_error(format!("{builtin}: missing {label} argument")))
}

fn require_dict<'a>(
    value: Option<&'a VmValue>,
    builtin: &str,
) -> Result<&'a BTreeMap<String, VmValue>, VmError> {
    match value {
        Some(VmValue::Dict(map)) => Ok(map),
        Some(other) => Err(vm_error(format!(
            "{builtin}: expected dict, got {}",
            other.type_name()
        ))),
        None => Err(vm_error(format!("{builtin}: missing components argument"))),
    }
}

pub(crate) fn vm_error(message: impl Into<String>) -> VmError {
    VmError::Thrown(VmValue::String(std::sync::Arc::from(message.into())))
}

fn utc_datetime_dict(dt: DateTime<Utc>) -> BTreeMap<String, VmValue> {
    let mut result = BTreeMap::new();
    result.insert("year".to_string(), VmValue::Int(dt.year() as i64));
    result.insert("month".to_string(), VmValue::Int(dt.month() as i64));
    result.insert("day".to_string(), VmValue::Int(dt.day() as i64));
    result.insert("hour".to_string(), VmValue::Int(dt.hour() as i64));
    result.insert("minute".to_string(), VmValue::Int(dt.minute() as i64));
    result.insert("second".to_string(), VmValue::Int(dt.second() as i64));
    result.insert(
        "weekday".to_string(),
        VmValue::Int(dt.weekday().num_days_from_sunday() as i64),
    );
    result
}

fn zoned_datetime_dict(dt: DateTime<Tz>) -> BTreeMap<String, VmValue> {
    let mut result = BTreeMap::new();
    result.insert("year".to_string(), VmValue::Int(dt.year() as i64));
    result.insert("month".to_string(), VmValue::Int(dt.month() as i64));
    result.insert("day".to_string(), VmValue::Int(dt.day() as i64));
    result.insert("hour".to_string(), VmValue::Int(dt.hour() as i64));
    result.insert("minute".to_string(), VmValue::Int(dt.minute() as i64));
    result.insert("second".to_string(), VmValue::Int(dt.second() as i64));
    result.insert(
        "weekday".to_string(),
        VmValue::Int(dt.weekday().num_days_from_sunday() as i64),
    );
    result.insert(
        "offset_seconds".to_string(),
        VmValue::Int(dt.offset().fix().local_minus_utc() as i64),
    );
    result.insert(
        "timestamp".to_string(),
        timestamp_value(dt.with_timezone(&Utc)),
    );
    result.insert(
        "iso8601".to_string(),
        VmValue::String(std::sync::Arc::from(dt.to_rfc3339())),
    );
    result
}

pub(crate) fn parse_timezone(raw: &str, builtin: &str) -> Result<Tz, VmError> {
    raw.parse::<Tz>()
        .map_err(|_| vm_error(format!("{builtin}: unknown timezone '{raw}'")))
}

fn parse_datetime_auto(input: &str) -> Result<DateTime<Utc>, VmError> {
    let trimmed = input.trim();
    if trimmed.is_empty() {
        return Err(vm_error("Cannot parse date: "));
    }

    if let Ok(dt) = DateTime::parse_from_rfc3339(trimmed) {
        return Ok(dt.with_timezone(&Utc));
    }

    for fmt in [
        "%Y-%m-%d %H:%M:%S%.f",
        "%Y-%m-%dT%H:%M:%S%.f",
        "%Y-%m-%d %H:%M",
        "%Y-%m-%dT%H:%M",
    ] {
        if let Ok(dt) = NaiveDateTime::parse_from_str(trimmed, fmt) {
            return Ok(Utc.from_utc_datetime(&dt));
        }
    }

    if let Ok(date) = NaiveDate::parse_from_str(trimmed, "%Y-%m-%d") {
        let dt = date
            .and_hms_opt(0, 0, 0)
            .ok_or_else(|| vm_error(format!("Cannot parse date: {input}")))?;
        return Ok(Utc.from_utc_datetime(&dt));
    }

    parse_datetime_digits_fallback(trimmed)
}

fn parse_datetime_digits_fallback(input: &str) -> Result<DateTime<Utc>, VmError> {
    let parts: Vec<i64> = input
        .split(|c: char| !c.is_ascii_digit())
        .filter_map(|p| if p.is_empty() { None } else { p.parse().ok() })
        .collect();
    if parts.len() < 3 {
        return Err(vm_error(format!("Cannot parse date: {input}")));
    }

    let year =
        i32::try_from(parts[0]).map_err(|_| vm_error(format!("Invalid year: {}", parts[0])))?;
    let month = u32::try_from(parts[1]).unwrap_or(0);
    let day = u32::try_from(parts[2]).unwrap_or(0);
    let hour = u32::try_from(parts.get(3).copied().unwrap_or(0)).unwrap_or(u32::MAX);
    let minute = u32::try_from(parts.get(4).copied().unwrap_or(0)).unwrap_or(u32::MAX);
    let second = u32::try_from(parts.get(5).copied().unwrap_or(0)).unwrap_or(u32::MAX);

    validate_component_ranges(month, day, hour, minute, second)?;
    let date = NaiveDate::from_ymd_opt(year, month, day)
        .ok_or_else(|| vm_error(format!("Invalid date: {year:04}-{month:02}-{day:02}")))?;
    let dt = date
        .and_hms_opt(hour, minute, second)
        .ok_or_else(|| vm_error(format!("Invalid time: {hour:02}:{minute:02}:{second:02}")))?;
    Ok(Utc.from_utc_datetime(&dt))
}

fn validate_component_ranges(
    month: u32,
    day: u32,
    hour: u32,
    minute: u32,
    second: u32,
) -> Result<(), VmError> {
    if !(1..=12).contains(&month) {
        return Err(vm_error(format!("Invalid month: {month} (must be 1-12)")));
    }
    if !(1..=31).contains(&day) {
        return Err(vm_error(format!("Invalid day: {day} (must be 1-31)")));
    }
    if hour > 23 {
        return Err(vm_error(format!("Invalid hour: {hour} (must be 0-23)")));
    }
    if minute > 59 {
        return Err(vm_error(format!("Invalid minute: {minute} (must be 0-59)")));
    }
    if second > 59 {
        return Err(vm_error(format!("Invalid second: {second} (must be 0-59)")));
    }
    Ok(())
}

pub(crate) fn datetime_from_arg(
    value: Option<&VmValue>,
    builtin: &str,
) -> Result<DateTime<Utc>, VmError> {
    let value = value.ok_or_else(|| vm_error(format!("{builtin}: missing timestamp argument")))?;
    match value {
        VmValue::Dict(map) => datetime_from_arg(map.get("timestamp"), builtin),
        VmValue::Int(seconds) => DateTime::from_timestamp(*seconds, 0)
            .ok_or_else(|| vm_error(format!("{builtin}: timestamp out of range"))),
        VmValue::Float(seconds) => datetime_from_float(*seconds, builtin),
        other => Err(vm_error(format!(
            "{builtin}: expected timestamp number or date dict, got {}",
            other.type_name()
        ))),
    }
}

fn datetime_from_float(seconds: f64, builtin: &str) -> Result<DateTime<Utc>, VmError> {
    if !seconds.is_finite() {
        return Err(vm_error(format!("{builtin}: timestamp must be finite")));
    }
    let micros = (seconds * 1_000_000.0).round();
    if micros < i128::MIN as f64 || micros > i128::MAX as f64 {
        return Err(vm_error(format!("{builtin}: timestamp out of range")));
    }
    let micros = micros as i128;
    let secs = i64::try_from(micros.div_euclid(1_000_000))
        .map_err(|_| vm_error(format!("{builtin}: timestamp out of range")))?;
    let nanos = (micros.rem_euclid(1_000_000) as u32) * 1_000;
    DateTime::from_timestamp(secs, nanos)
        .ok_or_else(|| vm_error(format!("{builtin}: timestamp out of range")))
}

pub(crate) fn timestamp_value(dt: DateTime<Utc>) -> VmValue {
    if dt.timestamp_subsec_nanos() == 0 {
        VmValue::Int(dt.timestamp())
    } else {
        VmValue::Float(dt.timestamp() as f64 + f64::from(dt.timestamp_subsec_nanos()) / 1e9)
    }
}

fn timestamp_millis_from_arg(value: Option<&VmValue>, builtin: &str) -> Result<i128, VmError> {
    let dt = datetime_from_arg(value, builtin)?;
    Ok(i128::from(dt.timestamp()) * 1_000 + i128::from(dt.timestamp_subsec_millis()))
}

fn timestamp_millis_value(millis: i128) -> Result<VmValue, VmError> {
    if millis % 1_000 == 0 {
        let seconds =
            i64::try_from(millis / 1_000).map_err(|_| vm_error("timestamp value out of range"))?;
        Ok(VmValue::Int(seconds))
    } else {
        Ok(VmValue::Float(millis as f64 / 1_000.0))
    }
}

fn component_i64(
    map: &BTreeMap<String, VmValue>,
    key: &str,
    default: Option<i64>,
) -> Result<i64, VmError> {
    match map.get(key) {
        Some(VmValue::Int(value)) => Ok(*value),
        Some(VmValue::Float(value)) if value.fract() == 0.0 => Ok(*value as i64),
        Some(other) => Err(vm_error(format!(
            "date_from_components: {key} must be an integer, got {}",
            other.type_name()
        ))),
        None => default.ok_or_else(|| vm_error(format!("date_from_components: missing {key}"))),
    }
}

fn datetime_from_components(
    map: &BTreeMap<String, VmValue>,
    tz: Tz,
) -> Result<DateTime<Tz>, VmError> {
    let year = i32::try_from(component_i64(map, "year", None)?)
        .map_err(|_| vm_error("date_from_components: year out of range"))?;
    let month = u32::try_from(component_i64(map, "month", None)?).unwrap_or(0);
    let day = u32::try_from(component_i64(map, "day", None)?).unwrap_or(0);
    let hour = u32::try_from(component_i64(map, "hour", Some(0))?).unwrap_or(u32::MAX);
    let minute = u32::try_from(component_i64(map, "minute", Some(0))?).unwrap_or(u32::MAX);
    let second = u32::try_from(component_i64(map, "second", Some(0))?).unwrap_or(u32::MAX);
    validate_component_ranges(month, day, hour, minute, second)?;
    let date = NaiveDate::from_ymd_opt(year, month, day)
        .ok_or_else(|| vm_error(format!("Invalid date: {year:04}-{month:02}-{day:02}")))?;
    let naive = date
        .and_hms_opt(hour, minute, second)
        .ok_or_else(|| vm_error(format!("Invalid time: {hour:02}:{minute:02}:{second:02}")))?;
    match tz.from_local_datetime(&naive) {
        LocalResult::Single(dt) => Ok(dt),
        LocalResult::Ambiguous(earlier, _) => Ok(earlier),
        LocalResult::None => Err(vm_error(
            "date_from_components: local time does not exist in that timezone",
        )),
    }
}

fn duration_from_number(
    value: Option<&VmValue>,
    multiplier_ms: i64,
    builtin: &str,
) -> Result<i64, VmError> {
    let value = value.ok_or_else(|| vm_error(format!("{builtin}: missing value argument")))?;
    let number = match value {
        VmValue::Int(value) => value
            .checked_mul(multiplier_ms)
            .ok_or_else(|| vm_error(format!("{builtin}: duration overflow")))?,
        VmValue::Float(value) if value.is_finite() => {
            let millis = *value * multiplier_ms as f64;
            if millis < i64::MIN as f64 || millis > i64::MAX as f64 {
                return Err(vm_error(format!("{builtin}: duration overflow")));
            }
            millis.round() as i64
        }
        other => {
            return Err(vm_error(format!(
                "{builtin}: expected number, got {}",
                other.type_name()
            )));
        }
    };
    Ok(number)
}

fn require_duration(value: Option<&VmValue>, builtin: &str) -> Result<i64, VmError> {
    match value {
        Some(VmValue::Duration(ms)) => Ok(*ms),
        Some(other) => Err(vm_error(format!(
            "{builtin}: expected duration, got {}",
            other.type_name()
        ))),
        None => Err(vm_error(format!("{builtin}: missing duration argument"))),
    }
}

fn format_duration_human(duration: i64) -> String {
    if duration == 0 {
        return "0s".to_string();
    }

    let sign = if duration < 0 { "-" } else { "" };
    let mut remaining = duration.unsigned_abs();
    let units = [
        ("d", 86_400_000_u64),
        ("h", 3_600_000_u64),
        ("m", 60_000_u64),
        ("s", 1_000_u64),
        ("ms", 1_u64),
    ];
    let mut parts = Vec::new();
    for (label, size) in units {
        let count = remaining / size;
        if count > 0 {
            parts.push(format!("{count}{label}"));
            remaining %= size;
        }
        if parts.len() == 3 {
            break;
        }
    }
    format!("{sign}{}", parts.join(" "))
}
