// Copyright (c) 2026, Salesforce, Inc.,
// All rights reserved.
// For full license text, see the LICENSE.txt file

use crate::{
    runtime::{
        coercion::CoerceArguments, value::Value, Context, Prelude, RuntimeError, RuntimeErrorKind,
        ValueHandler,
    },
    Location,
};
use base64::Engine;
use std::iter::zip;
use std::rc::Rc;

use super::coercion::Coerce;

use crate::runtime::value::{Array, InternalValue, Object};
use base64::engine::general_purpose;

fn concat(location: Location, _: &dyn Context, arguments: &[Value]) -> Result<Value, RuntimeError> {
    let (a, b): (String, String) = arguments.coerce_arguments(location)?;
    let b = b.as_str();
    Ok(Value::string(a + b))
}

fn subtract(
    location: Location,
    _: &dyn Context,
    arguments: &[Value],
) -> Result<Value, RuntimeError> {
    let (container, content) = match arguments {
        [container, content] => Ok((container, content)),
        [] => Err(RuntimeError {
            location,
            kind: RuntimeErrorKind::NotEnoughArguments,
        }),
        _ => Err(RuntimeError {
            location,
            kind: RuntimeErrorKind::TooManyArguments,
        }),
    }?;

    match (&container.internal, &content.internal) {
        (InternalValue::Array(first), InternalValue::Array(second)) => {
            subtract_array_array(first, second)
        }
        (InternalValue::Object(first), InternalValue::Array(second)) => {
            subtract_object_array(location, first, second)
        }
        (InternalValue::Object(first), InternalValue::Object(second)) => {
            subtract_object_object(first, second)
        }
        (_, _) => Err(RuntimeError {
            location,
            kind: RuntimeErrorKind::TypeMismatch,
        }),
    }
}

fn subtract_array_array(first: &Rc<Array>, second: &Rc<Array>) -> Result<Value, RuntimeError> {
    let result = first
        .iter()
        .filter(|x| second.iter().all(|y| !compare_as_strs(x, y)))
        .cloned()
        .collect();

    Ok(Value::array(result))
}

fn cast_string_array(location: Location, array: &Rc<Array>) -> Result<Vec<String>, RuntimeError> {
    let mut result = Vec::new();
    for value in array.iter() {
        if let Some(value) = value.cast() {
            result.push(value);
        } else {
            return Err(RuntimeError {
                location,
                kind: RuntimeErrorKind::TypeMismatch,
            });
        }
    }

    Ok(result)
}

fn subtract_object_array(
    location: Location,
    first: &Rc<Object>,
    second: &Rc<Array>,
) -> Result<Value, RuntimeError> {
    let second = cast_string_array(location, second)?;

    let result = first
        .iter()
        .filter(|(key, _)| !second.contains(key))
        .map(|(key, value)| (key.clone(), value.clone()))
        .collect();

    Ok(Value::object(result))
}

fn subtract_object_object(first: &Rc<Object>, second: &Rc<Object>) -> Result<Value, RuntimeError> {
    let result = first
        .iter()
        .filter(|(key, first)| {
            second
                .get(*key)
                .map(|second| !compare_as_strs(first, second))
                .unwrap_or(true)
        })
        .map(|(key, value)| (key.clone(), value.clone()))
        .collect();

    Ok(Value::object(result))
}

/// Compares two values for equality. Leaf elements of collections are compared as strings.
fn compare_as_strs(first: &Value, second: &Value) -> bool {
    match (&first.internal, &second.internal) {
        (InternalValue::Object(first), InternalValue::Object(second)) => {
            first.len() == second.len()
                && first.iter().all(|(key, first)| {
                    second
                        .get(key)
                        .map(|second| compare_as_strs(first, second))
                        .unwrap_or(false)
                })
        }
        (InternalValue::Array(first), InternalValue::Array(second)) => {
            first.len() == second.len()
                && zip(first.iter(), second.iter())
                    .all(|(first, second)| compare_as_strs(first, second))
        }
        _ => {
            let first: Option<String> = first.cast();
            let second: Option<String> = second.cast();
            first == second
        }
    }
}

fn contains(
    location: Location,
    _: &dyn Context,
    arguments: &[Value],
) -> Result<Value, RuntimeError> {
    match arguments {
        [container, content] => {
            let result = if let Some(container) = container.as_str() {
                let pattern: String = content.coerce(location)?;
                container.contains(&pattern)
            } else if let Some(array) = container.as_slice() {
                array.contains(content)
            } else {
                let container: String = container.coerce(location)?;
                let pattern: String = content.coerce(location)?;
                container.contains(&pattern)
            };
            Ok(Value::bool(result))
        }
        [] => Err(RuntimeError {
            location,
            kind: RuntimeErrorKind::NotEnoughArguments,
        }),
        _ => Err(RuntimeError {
            location,
            kind: RuntimeErrorKind::TooManyArguments,
        }),
    }
}

fn lower(location: Location, _: &dyn Context, arguments: &[Value]) -> Result<Value, RuntimeError> {
    match arguments {
        [text] => {
            if text.is_null() {
                Ok(Value::null())
            } else {
                let text: String = text.coerce(location)?;
                Ok(Value::string(text.to_lowercase()))
            }
        }
        [] => Err(RuntimeError {
            location,
            kind: RuntimeErrorKind::NotEnoughArguments,
        }),
        _ => Err(RuntimeError {
            location,
            kind: RuntimeErrorKind::TooManyArguments,
        }),
    }
}

fn trim(location: Location, _: &dyn Context, arguments: &[Value]) -> Result<Value, RuntimeError> {
    let (s,): (String,) = arguments.coerce_arguments(location)?;
    Ok(Value::string(s.trim().to_string()))
}

fn uuid_v4(
    location: Location,
    _: &dyn Context,
    arguments: &[Value],
) -> Result<Value, RuntimeError> {
    if !arguments.is_empty() {
        return Err(RuntimeError {
            location,
            kind: super::RuntimeErrorKind::TooManyArguments,
        });
    }

    let mut bytes = [0u8; 16];
    getrandom::getrandom(&mut bytes).map_err(|_| RuntimeError {
        location,
        kind: RuntimeErrorKind::UnavailableRandomGenerator,
    })?;

    let mut builder = uuid::Builder::from_bytes(bytes);
    let uuid = builder
        .set_variant(uuid::Variant::RFC4122)
        .set_version(uuid::Version::Random)
        .as_uuid();

    Ok(Value::string(uuid.to_string()))
}

fn size_of(
    location: Location,
    context: &dyn Context,
    arguments: &[Value],
) -> Result<Value, RuntimeError> {
    let object = arguments.first().ok_or(RuntimeError {
        location,
        kind: RuntimeErrorKind::TooManyArguments,
    })?;
    let size = object
        .to_value_handler(context)
        .and_then(|vh| vh.size())
        .ok_or(RuntimeError {
            location,
            kind: RuntimeErrorKind::UnavailableSize,
        })?;
    Ok(Value::number(size as f64))
}

fn is_empty(
    location: Location,
    _: &dyn Context,
    arguments: &[Value],
) -> Result<Value, RuntimeError> {
    let object = arguments.first().ok_or(RuntimeError {
        location,
        kind: RuntimeErrorKind::NotEnoughArguments,
    })?;
    Ok(Value::bool(object.is_empty()))
}

fn split_by(
    location: Location,
    _: &dyn Context,
    arguments: &[Value],
) -> Result<Value, RuntimeError> {
    match arguments {
        [text, separator] => {
            if text.is_null() {
                Ok(Value::null())
            } else {
                let text: String = text.coerce(location)?;
                let separator: String = separator.coerce(location)?;
                if separator.is_empty() {
                    let s = text.chars().map(|s| Value::string(s.to_string())).collect();
                    Ok(Value::array(s))
                } else {
                    let s = text
                        .split(&separator)
                        .map(|s| Value::string(s.to_string()))
                        .collect();
                    Ok(Value::array(s))
                }
            }
        }
        [] => Err(RuntimeError {
            location,
            kind: RuntimeErrorKind::NotEnoughArguments,
        }),
        _ => Err(RuntimeError {
            location,
            kind: RuntimeErrorKind::TooManyArguments,
        }),
    }
}

fn substring_after(
    location: Location,
    _: &dyn Context,
    arguments: &[Value],
) -> Result<Value, RuntimeError> {
    match arguments {
        [text, separator] => {
            if text.is_null() {
                Ok(Value::null())
            } else {
                let text: String = text.coerce(location)?;
                let separator: String = separator.coerce(location)?;
                if let Some((_, result)) = text.split_once(&separator) {
                    Ok(Value::string(result.to_string()))
                } else {
                    Ok(Value::string("".to_string()))
                }
            }
        }
        [] => Err(RuntimeError {
            location,
            kind: RuntimeErrorKind::NotEnoughArguments,
        }),
        _ => Err(RuntimeError {
            location,
            kind: RuntimeErrorKind::TooManyArguments,
        }),
    }
}

fn substring_after_last(
    location: Location,
    _: &dyn Context,
    arguments: &[Value],
) -> Result<Value, RuntimeError> {
    match arguments {
        [text, separator] => {
            if text.is_null() {
                Ok(Value::null())
            } else {
                let text: String = text.coerce(location)?;
                let separator: String = separator.coerce(location)?;
                if let Some((_, result)) = text.rsplit_once(&separator) {
                    Ok(Value::string(result.to_string()))
                } else {
                    Ok(Value::string("".to_string()))
                }
            }
        }
        [] => Err(RuntimeError {
            location,
            kind: RuntimeErrorKind::NotEnoughArguments,
        }),
        _ => Err(RuntimeError {
            location,
            kind: RuntimeErrorKind::TooManyArguments,
        }),
    }
}

fn substring_before(
    location: Location,
    _: &dyn Context,
    arguments: &[Value],
) -> Result<Value, RuntimeError> {
    match arguments {
        [text, separator] => {
            if text.is_null() {
                Ok(Value::null())
            } else {
                let text: String = text.coerce(location)?;
                let separator: String = separator.coerce(location)?;
                if let Some((result, _)) = text.split_once(&separator) {
                    Ok(Value::string(result.to_string()))
                } else {
                    Ok(Value::string("".to_string()))
                }
            }
        }
        [] => Err(RuntimeError {
            location,
            kind: RuntimeErrorKind::NotEnoughArguments,
        }),
        _ => Err(RuntimeError {
            location,
            kind: RuntimeErrorKind::TooManyArguments,
        }),
    }
}

fn substring_before_last(
    location: Location,
    _: &dyn Context,
    arguments: &[Value],
) -> Result<Value, RuntimeError> {
    match arguments {
        [text, separator] => {
            if text.is_null() {
                Ok(Value::null())
            } else {
                let text: String = text.coerce(location)?;
                let separator: String = separator.coerce(location)?;
                if separator.is_empty() && !text.is_empty() {
                    Ok(Value::string(text[0..text.len() - 1].to_string()))
                } else if let Some((result, _)) = text.rsplit_once(&separator) {
                    Ok(Value::string(result.to_string()))
                } else {
                    Ok(Value::string("".to_string()))
                }
            }
        }
        [] => Err(RuntimeError {
            location,
            kind: RuntimeErrorKind::NotEnoughArguments,
        }),
        _ => Err(RuntimeError {
            location,
            kind: RuntimeErrorKind::TooManyArguments,
        }),
    }
}

fn upper(location: Location, _: &dyn Context, arguments: &[Value]) -> Result<Value, RuntimeError> {
    match arguments {
        [text] => {
            if text.is_null() {
                Ok(Value::null())
            } else {
                let text: String = text.coerce(location)?;
                Ok(Value::string(text.to_uppercase()))
            }
        }
        [] => Err(RuntimeError {
            location,
            kind: RuntimeErrorKind::NotEnoughArguments,
        }),
        _ => Err(RuntimeError {
            location,
            kind: RuntimeErrorKind::TooManyArguments,
        }),
    }
}

fn to_base64(
    location: Location,
    _: &dyn Context,
    arguments: &[Value],
) -> Result<Value, RuntimeError> {
    match arguments {
        [binary] => {
            let bytes: Vec<u8> = binary.coerce(location)?;
            Ok(Value::string(general_purpose::STANDARD.encode(bytes)))
        }
        [] => Err(RuntimeError {
            location,
            kind: RuntimeErrorKind::NotEnoughArguments,
        }),
        _ => Err(RuntimeError {
            location,
            kind: RuntimeErrorKind::TooManyArguments,
        }),
    }
}

fn from_base64(
    location: Location,
    _: &dyn Context,
    arguments: &[Value],
) -> Result<Value, RuntimeError> {
    match arguments {
        [text] => {
            let text: String = text.coerce(location)?;
            let unpadded_text: String = text.trim_end_matches('=').to_string();
            Ok(Value::binary(
                general_purpose::STANDARD_NO_PAD
                    .decode(&unpadded_text)
                    .map_err(|_| RuntimeError {
                        location,
                        kind: RuntimeErrorKind::TypeMismatch,
                    })?,
            ))
        }
        [] => Err(RuntimeError {
            location,
            kind: RuntimeErrorKind::NotEnoughArguments,
        }),
        _ => Err(RuntimeError {
            location,
            kind: RuntimeErrorKind::TooManyArguments,
        }),
    }
}

fn to_string(
    location: Location,
    _: &dyn Context,
    arguments: &[Value],
) -> Result<Value, RuntimeError> {
    match arguments {
        [binary, encoding] => {
            let encoding: String = encoding.coerce(location)?;
            if !encoding.eq_ignore_ascii_case("UTF-8") {
                return Err(RuntimeError {
                    location,
                    kind: RuntimeErrorKind::TypeMismatch,
                });
            };

            let coerced: String = binary.coerce(location)?;
            Ok(Value::string(coerced))
        }
        [] | [_] => Err(RuntimeError {
            location,
            kind: RuntimeErrorKind::NotEnoughArguments,
        }),
        _ => Err(RuntimeError {
            location,
            kind: RuntimeErrorKind::TooManyArguments,
        }),
    }
}

fn to_binary(
    location: Location,
    _: &dyn Context,
    arguments: &[Value],
) -> Result<Value, RuntimeError> {
    match arguments {
        [text, encoding] => {
            let text: String = text.coerce(location)?;
            let encoding: String = encoding.coerce(location)?;

            if !encoding.eq_ignore_ascii_case("UTF-8") {
                return Err(RuntimeError {
                    location,
                    kind: RuntimeErrorKind::TypeMismatch,
                });
            };

            Ok(Value::binary(text.into_bytes()))
        }
        [] | [_] => Err(RuntimeError {
            location,
            kind: RuntimeErrorKind::NotEnoughArguments,
        }),
        _ => Err(RuntimeError {
            location,
            kind: RuntimeErrorKind::TooManyArguments,
        }),
    }
}

type PreludeFunction = fn(Location, &dyn Context, &[Value]) -> Result<Value, RuntimeError>;

static PRELUDE: &[(&str, PreludeFunction)] = &[
    ("++", concat),
    ("--", subtract),
    ("contains", contains),
    ("lower", lower),
    ("sizeOf", size_of),
    ("splitBy", split_by),
    ("substringAfter", substring_after),
    ("substringAfterLast", substring_after_last),
    ("substringBefore", substring_before),
    ("substringBeforeLast", substring_before_last),
    ("toBase64", to_base64),
    ("fromBase64", from_base64),
    ("toString", to_string),
    ("toBinary", to_binary),
    ("trim", trim),
    ("upper", upper),
    ("uuid", uuid_v4),
    ("isEmpty", is_empty),
];

pub fn prelude() -> Prelude {
    PRELUDE
        .iter()
        .map(|(key, function)| (*key, Value::function_from_fn(function)))
        .collect()
}

#[cfg(test)]
mod tests {
    use crate::runtime::{Binding, ValueHandler};

    use super::{concat, split_by, trim, Context, Location, Value};

    struct TestContext;

    impl Context for TestContext {
        fn resolve(&self, _: &crate::expression::Symbol) -> Binding {
            unreachable!()
        }

        fn value_handler(&self, _reference: crate::Reference) -> Option<&dyn ValueHandler> {
            unreachable!()
        }
    }

    const LOCATION: Location = Location {
        start: 100,
        end: 200,
    };

    const CONTEXT: &dyn Context = &TestContext;

    #[test]
    fn trim_string() {
        let result = trim(
            LOCATION,
            CONTEXT,
            &[Value::string("  hello world  ".to_string())],
        )
        .unwrap();
        assert_eq!("hello world", result.as_str().unwrap());
    }

    #[test]
    fn concat_strings() {
        let result = concat(
            LOCATION,
            CONTEXT,
            &[
                Value::string("hello ".to_string()),
                Value::string("world".to_string()),
            ],
        )
        .unwrap();
        assert_eq!("hello world", result.as_str().unwrap());
    }

    #[test]
    fn concat_numbers() {
        let result = concat(
            LOCATION,
            CONTEXT,
            &[Value::number(15.04), Value::number(12.01)],
        )
        .unwrap();
        assert_eq!("15.0412.01", result.as_str().unwrap());
    }

    #[test]
    fn concat_booleans() {
        let result = concat(LOCATION, CONTEXT, &[Value::bool(true), Value::bool(false)]).unwrap();
        assert_eq!("truefalse", result.as_str().unwrap());
    }

    #[test]
    fn fail_when_concat_null() {
        let result = concat(LOCATION, CONTEXT, &[Value::null(), Value::bool(false)]);
        assert!(result.is_err());
    }

    #[test]
    fn split_by_strings() {
        let result = split_by(
            LOCATION,
            CONTEXT,
            &[
                Value::string("Peregrine Expression Language".to_string()),
                Value::string("re".to_string()),
            ],
        )
        .unwrap();
        let expected = &[
            Value::string("Pe".to_string()),
            Value::string("grine Exp".to_string()),
            Value::string("ssion Language".to_string()),
        ];
        assert_eq!(expected, result.as_slice().unwrap());
    }
}
