use std::cmp::{max, min};

use anyhow::{anyhow, bail};

use crate::func::def::{MatchTestFailure, ParameterBindingProvider};
use crate::types::decimal_type::Decimal;
use crate::types::{Type, DOUBLE, INT, UNKNOWN};

/// A helper that checks if all arguments are of the same type.
///
/// Note: this handles "merging" null's UNKNOWN type.
pub fn bindings_must_have_same_type(
    bindings: &dyn ParameterBindingProvider,
) -> anyhow::Result<Type> {
    must_be_same_type(bindings.iter().map(|binding| binding.typ()))
}

/// A helper that checks if the given translations are of the same type.
///
/// Note: this handles "merging" null's UNKNOWN type but explicitly disallows struct merging.
pub fn must_be_same_type<'a, I>(types: I) -> anyhow::Result<Type>
where
    I: IntoIterator<Item = &'a Type>,
{
    types.into_iter().try_fold(UNKNOWN, |existing, current| {
        match (&existing, current) {
            // Equal types - return either one
            (left, right) if left == right => Ok(existing),

            // Unknown handling - unknown can merge with any type
            (Type::Unknown, right) => Ok(right.clone()),
            (left, Type::Unknown) => Ok(left.clone()),

            // Struct types are not mergeable - explicitly disallow
            (Type::Struct(_), Type::Struct(_)) => Err(anyhow!(MatchTestFailure(
                anyhow!("Struct types cannot be merged").into()
            ))),

            // Try to merge other compatible types
            (_, right) => existing
                .clone()
                .merge(right.clone())
                .map_err(|e| anyhow!(MatchTestFailure::wrap(e))),
        }
    })
}

/// Compute the return type as the 0th argument (with its scale removed, if applicable)
pub fn drop_decimal_scale(bindings: &dyn ParameterBindingProvider) -> anyhow::Result<Type> {
    match bindings.get_by_index(0)?.typ() {
        Type::Decimal(d) => Ok(Decimal::new(d.precision, 0)?.into()),
        t => Ok(t.clone()),
    }
}

/// Compute the return type as the 0th argument (with its scale removed, if applicable)
pub fn pls_min_dec(dec: &Decimal) -> anyhow::Result<Decimal> {
    Decimal::new(min(max(dec.precision + 1, 12), 38), dec.scale)
}

/// Compute the merged decimal() return type for decimal + decimal or decimal - decimal.
pub fn pls_min_dec_merge(left: &Decimal, right: &Decimal) -> anyhow::Result<Decimal> {
    Decimal::new(
        min(
            38,
            1 + max(left.scale, right.scale)
                + max(left.precision - left.scale, right.precision - right.scale),
        ),
        max(left.scale, right.scale),
    )
}

/// Lambda generator for computing the return type of a numeric binary operation
///
/// This follows the "doubles poison" logic -- if there's a double involved, your return type is
/// a double. If there are two ints, the return type is an int. If there is a decimal and an int,
/// or if there are two decimals, this function delegates to the passed helpers.
///
/// # Arguments
/// - `convert` - Behavior for converting a decimal when it is called with an int.
/// - `merge` - Behavior for merging two decimals.
///
/// # Returns
/// The lambda that you can pass to FunctionDef::with_match_test
pub fn doubles_poison_and<C, M>(
    convert: C,
    merge: M,
) -> impl Fn(&dyn ParameterBindingProvider) -> anyhow::Result<Type>
where
    C: Fn(&Decimal) -> anyhow::Result<Decimal>,
    M: Fn(&Decimal, &Decimal) -> anyhow::Result<Decimal>,
{
    move |bindings| {
        let left_type = bindings.get_by_index(0)?.typ().clone();
        let right_type = bindings.get_by_index(1)?.typ().clone();

        match (left_type, right_type) {
            (Type::Decimal(left), Type::Decimal(right)) => Ok(merge(&left, &right)?.into()),
            (Type::Decimal(left), Type::Int) => Ok(convert(&left)?.into()),
            (Type::Int, Type::Decimal(right)) => Ok(convert(&right)?.into()),
            (Type::Double, _) | (_, Type::Double) => Ok(DOUBLE.into()),
            (Type::Int, Type::Int) => Ok(INT.into()),
            _ => {
                bail!(MatchTestFailure(
                    anyhow!("arguments are not numeric").into()
                ));
            }
        }
    }
}
