use super::*;

pub(super) fn resolve(value: &Value, data: &Value) -> Result<Value, String> {
    resolve_at(value, data, data)
}

pub(super) fn resolve_at(value: &Value, data: &Value, context: &Value) -> Result<Value, String> {
    if let Some(path) = value
        .as_object()
        .and_then(|value| value.get("path"))
        .and_then(Value::as_str)
    {
        let source = if path.starts_with('/') { data } else { context };
        let pointer = if path.starts_with('/') {
            normalize_pointer(path).to_owned()
        } else {
            format!("/{path}")
        };
        return Ok(source.pointer(&pointer).cloned().unwrap_or(Value::Null));
    }
    if let Some(call) = value
        .as_object()
        .and_then(|value| value.get("call"))
        .and_then(Value::as_str)
    {
        return evaluate(
            call,
            value.get("args").unwrap_or(&Value::Null),
            data,
            context,
        );
    }
    match value {
        Value::Array(values) => values
            .iter()
            .map(|value| resolve_at(value, data, context))
            .collect::<Result<Vec<_>, _>>()
            .map(Value::Array),
        Value::Object(values) => values
            .iter()
            .map(|(key, value)| Ok((key.clone(), resolve_at(value, data, context)?)))
            .collect::<Result<Map<_, _>, String>>()
            .map(Value::Object),
        _ => Ok(value.clone()),
    }
}

pub(super) fn evaluate(
    call: &str,
    args: &Value,
    data: &Value,
    context: &Value,
) -> Result<Value, String> {
    if !FUNCTIONS.contains(&call) {
        return Err(format!("function `{call}` is not declared by the catalog"));
    }
    let args = args
        .as_object()
        .ok_or_else(|| format!("function `{call}` requires object args"))?;
    let value = || resolve_at(args.get("value").unwrap_or(&Value::Null), data, context);
    match call {
        "required" => Ok(Value::Bool(match value()? {
            Value::Null => false,
            Value::String(value) => !value.is_empty(),
            Value::Array(value) => !value.is_empty(),
            Value::Object(value) => !value.is_empty(),
            _ => true,
        })),
        "regex" => {
            let pattern = args
                .get("pattern")
                .and_then(Value::as_str)
                .ok_or_else(|| "regex requires a pattern".to_owned())?;
            let regex = Regex::new(pattern).map_err(|error| format!("invalid regex: {error}"))?;
            Ok(Value::Bool(regex.is_match(&display_value(&value()?))))
        }
        "length" => {
            let length = display_value(&value()?).chars().count() as u64;
            let min = args.get("min").and_then(Value::as_u64).unwrap_or(0);
            let max = args.get("max").and_then(Value::as_u64).unwrap_or(u64::MAX);
            Ok(Value::Bool((min..=max).contains(&length)))
        }
        "numeric" => {
            let resolved = value()?;
            let number = resolved
                .as_f64()
                .or_else(|| resolved.as_str().and_then(|value| value.parse().ok()));
            let min = args
                .get("min")
                .and_then(Value::as_f64)
                .unwrap_or(f64::NEG_INFINITY);
            let max = args
                .get("max")
                .and_then(Value::as_f64)
                .unwrap_or(f64::INFINITY);
            Ok(Value::Bool(
                number.is_some_and(|number| (min..=max).contains(&number)),
            ))
        }
        "email" => {
            let email = display_value(&value()?);
            let valid = Regex::new(r"^[^\s@]+@[^\s@]+\.[^\s@]+$")
                .unwrap()
                .is_match(&email);
            Ok(Value::Bool(valid))
        }
        "formatString" => Ok(Value::String(interpolate(
            &display_value(&value()?),
            data,
            context,
        )?)),
        "formatNumber" => {
            let number = value()?.as_f64().unwrap_or(0.0);
            let digits = resolve_at(args.get("decimals").unwrap_or(&json!(2)), data, context)?
                .as_f64()
                .map(|value| value.max(0.0) as u64)
                .unwrap_or(2)
                .min(12) as usize;
            let formatted = format!("{number:.digits$}");
            Ok(Value::String(
                if resolve_at(
                    args.get("grouping").unwrap_or(&Value::Bool(true)),
                    data,
                    context,
                )?
                .as_bool()
                .unwrap_or(true)
                {
                    group_number(&formatted)
                } else {
                    formatted
                },
            ))
        }
        "formatCurrency" => {
            let number = value()?.as_f64().unwrap_or(0.0);
            let currency = display_value(&resolve_at(
                args.get("currency").unwrap_or(&Value::Null),
                data,
                context,
            )?);
            let digits = resolve_at(args.get("decimals").unwrap_or(&json!(2)), data, context)?
                .as_f64()
                .map(|value| value.max(0.0) as u64)
                .unwrap_or(2)
                .min(12) as usize;
            let formatted = format!("{number:.digits$}");
            let number = if resolve_at(
                args.get("grouping").unwrap_or(&Value::Bool(true)),
                data,
                context,
            )?
            .as_bool()
            .unwrap_or(true)
            {
                group_number(&formatted)
            } else {
                formatted
            };
            Ok(Value::String(format!("{currency} {number}")))
        }
        "formatDate" => {
            let input = display_value(&value()?);
            let date =
                time::OffsetDateTime::parse(&input, &time::format_description::well_known::Rfc3339)
                    .map_err(|error| format!("formatDate requires an RFC 3339 value: {error}"))?;
            let pattern = display_value(&resolve_at(
                args.get("format").unwrap_or(&Value::Null),
                data,
                context,
            )?);
            Ok(Value::String(format_date(date, &pattern)))
        }
        "pluralize" => {
            let number = value()?.as_f64().unwrap_or(0.0);
            let key = if number == 0.0 && args.contains_key("zero") {
                "zero"
            } else if number == 1.0 && args.contains_key("one") {
                "one"
            } else if number == 2.0 && args.contains_key("two") {
                "two"
            } else {
                "other"
            };
            resolve_at(args.get(key).unwrap_or(&Value::Null), data, context)
        }
        "and" | "or" => {
            let values = args
                .get("values")
                .and_then(Value::as_array)
                .ok_or_else(|| format!("{call} requires array `values`"))?;
            let values = values
                .iter()
                .map(|value| {
                    resolve_at(value, data, context)?
                        .as_bool()
                        .ok_or_else(|| format!("{call} requires boolean values"))
                })
                .collect::<Result<Vec<_>, _>>()?;
            Ok(Value::Bool(if call == "and" {
                values.into_iter().all(|value| value)
            } else {
                values.into_iter().any(|value| value)
            }))
        }
        "not" => {
            Ok(Value::Bool(!value()?.as_bool().ok_or_else(|| {
                "not requires a boolean value".to_owned()
            })?))
        }
        "openUrl" => Ok(Value::Null),
        "@index" => {
            let index = TEMPLATE_INDEX
                .with(Cell::get)
                .ok_or_else(|| "@index is only available in a list template".to_owned())?;
            let offset = args
                .get("offset")
                .map(|value| resolve_at(value, data, context))
                .transpose()?
                .and_then(|value| value.as_i64())
                .unwrap_or(0);
            Ok(json!(index as i64 + offset))
        }
        _ => unreachable!(),
    }
}

fn interpolate(template: &str, data: &Value, context: &Value) -> Result<String, String> {
    let mut output = String::new();
    let mut offset = 0;
    while let Some(relative) = template[offset..].find("${") {
        let start = offset + relative;
        if start > offset && template.as_bytes()[start - 1] == b'\\' {
            output.push_str(&template[offset..start - 1]);
            output.push_str("${");
            offset = start + 2;
            continue;
        }
        output.push_str(&template[offset..start]);
        let end = expression_end(template, start + 2)
            .ok_or_else(|| "formatString contains an unclosed expression".to_owned())?;
        output.push_str(&display_value(&evaluate_expression(
            &template[start + 2..end],
            data,
            context,
        )?));
        offset = end + 1;
    }
    output.push_str(&template[offset..]);
    Ok(output)
}

fn expression_end(text: &str, mut offset: usize) -> Option<usize> {
    let mut depth = 1;
    let mut quote = None;
    while offset < text.len() {
        let character = text[offset..].chars().next()?;
        if let Some(current) = quote {
            if character == current && text.as_bytes().get(offset.wrapping_sub(1)) != Some(&b'\\') {
                quote = None;
            }
        } else if matches!(character, '\'' | '"') {
            quote = Some(character);
        } else if text[offset..].starts_with("${") {
            depth += 1;
            offset += 2;
            continue;
        } else if character == '}' {
            depth -= 1;
            if depth == 0 {
                return Some(offset);
            }
        }
        offset += character.len_utf8();
    }
    None
}

fn evaluate_expression(expression: &str, data: &Value, context: &Value) -> Result<Value, String> {
    let expression = expression.trim();
    if let Some(open) = expression.find('(')
        && expression.ends_with(')')
    {
        let call = expression[..open].trim();
        let mut args = Map::new();
        for argument in split_expression_args(&expression[open + 1..expression.len() - 1]) {
            let colon = top_level_separator(argument, ':')
                .ok_or_else(|| format!("formatString argument `{argument}` must be named"))?;
            let name = argument[..colon].trim();
            if name.is_empty() {
                return Err("formatString contains an empty argument name".into());
            }
            args.insert(
                name.to_owned(),
                expression_value(argument[colon + 1..].trim(), data, context)?,
            );
        }
        return evaluate(call, &Value::Object(args), data, context);
    }
    let (source, pointer) = if expression.starts_with('/') {
        (data, normalize_pointer(expression).to_owned())
    } else {
        (context, format!("/{expression}"))
    };
    Ok(source.pointer(&pointer).cloned().unwrap_or(Value::Null))
}

fn split_expression_args(input: &str) -> Vec<&str> {
    let mut arguments = Vec::new();
    let mut start = 0;
    while let Some(relative) = top_level_separator(&input[start..], ',') {
        arguments.push(input[start..start + relative].trim());
        start += relative + 1;
    }
    if !input[start..].trim().is_empty() {
        arguments.push(input[start..].trim());
    }
    arguments
}

fn top_level_separator(input: &str, separator: char) -> Option<usize> {
    let mut round = 0;
    let mut braces = 0;
    let mut quote = None;
    for (index, character) in input.char_indices() {
        if let Some(current) = quote {
            if character == current && input.as_bytes().get(index.wrapping_sub(1)) != Some(&b'\\') {
                quote = None;
            }
            continue;
        }
        match character {
            '\'' | '"' => quote = Some(character),
            '(' => round += 1,
            ')' => round -= 1,
            '{' => braces += 1,
            '}' => braces -= 1,
            _ if character == separator && round == 0 && braces == 0 => return Some(index),
            _ => {}
        }
    }
    None
}

fn expression_value(expression: &str, data: &Value, context: &Value) -> Result<Value, String> {
    if expression.starts_with("${") && expression.ends_with('}') {
        return evaluate_expression(&expression[2..expression.len() - 1], data, context);
    }
    if expression.starts_with('\'') && expression.ends_with('\'') && expression.len() >= 2 {
        return Ok(Value::String(
            expression[1..expression.len() - 1].to_owned(),
        ));
    }
    if let Ok(value) = serde_json::from_str(expression) {
        return Ok(value);
    }
    if expression.starts_with('/') {
        return Ok(data
            .pointer(normalize_pointer(expression))
            .cloned()
            .unwrap_or(Value::Null));
    }
    Ok(Value::String(expression.to_owned()))
}

fn group_number(number: &str) -> String {
    let (whole, fraction) = number.split_once('.').unwrap_or((number, ""));
    let (sign, digits) = whole
        .strip_prefix('-')
        .map_or(("", whole), |digits| ("-", digits));
    let mut grouped = String::with_capacity(number.len() + number.len() / 3);
    grouped.push_str(sign);
    for (index, digit) in digits.chars().enumerate() {
        if index > 0 && (digits.len() - index).is_multiple_of(3) {
            grouped.push(',');
        }
        grouped.push(digit);
    }
    if !fraction.is_empty() {
        grouped.push('.');
        grouped.push_str(fraction);
    }
    grouped
}

pub(super) fn format_date(date: time::OffsetDateTime, pattern: &str) -> String {
    const MONTHS: [&str; 12] = [
        "January",
        "February",
        "March",
        "April",
        "May",
        "June",
        "July",
        "August",
        "September",
        "October",
        "November",
        "December",
    ];
    const DAYS: [&str; 7] = [
        "Monday",
        "Tuesday",
        "Wednesday",
        "Thursday",
        "Friday",
        "Saturday",
        "Sunday",
    ];
    let month = MONTHS[date.month() as usize - 1];
    let day = DAYS[date.weekday().number_days_from_monday() as usize];
    let hour_12 = match date.hour() % 12 {
        0 => 12,
        hour => hour,
    };
    let replacements = [
        ("EEEE", day.to_owned()),
        ("MMMM", month.to_owned()),
        ("yyyy", format!("{:04}", date.year())),
        ("MMM", month[..3].to_owned()),
        ("yy", format!("{:02}", date.year().rem_euclid(100))),
        ("MM", format!("{:02}", date.month() as u8)),
        ("dd", format!("{:02}", date.day())),
        ("HH", format!("{:02}", date.hour())),
        ("hh", format!("{hour_12:02}")),
        ("mm", format!("{:02}", date.minute())),
        ("ss", format!("{:02}", date.second())),
        ("E", day[..3].to_owned()),
        ("M", (date.month() as u8).to_string()),
        ("d", date.day().to_string()),
        ("H", date.hour().to_string()),
        ("h", hour_12.to_string()),
        ("a", if date.hour() < 12 { "AM" } else { "PM" }.to_owned()),
    ];
    let mut output = String::new();
    let mut remaining = pattern;
    while !remaining.is_empty() {
        if let Some((token, replacement)) = replacements
            .iter()
            .find(|(token, _)| remaining.starts_with(token))
        {
            output.push_str(replacement);
            remaining = &remaining[token.len()..];
        } else {
            let character = remaining.chars().next().unwrap();
            output.push(character);
            remaining = &remaining[character.len_utf8()..];
        }
    }
    output
}
