use alloc::{format, string::String, vec::Vec};

use crate::{Data, HostValue};

pub trait DecodalSchema {
    fn decodal_schema() -> HostValue;
}

pub trait DecodalDecode: Sized {
    fn decodal_decode(data: &Data) -> DecodeResult<Self>;
}

pub trait IntoHostValue {
    fn into_host_value(self) -> HostValue;
}

pub type DecodeResult<T> = core::result::Result<T, DecodeError>;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DecodeError {
    pub path: String,
    pub message: String,
}

impl DecodeError {
    pub fn new(path: impl Into<String>, message: impl Into<String>) -> Self {
        Self {
            path: path.into(),
            message: message.into(),
        }
    }

    pub fn at_type(path: &str, expected: &'static str) -> Self {
        Self::new(path, format!("expected {expected}"))
    }
}

impl core::fmt::Display for DecodeError {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        if self.path.is_empty() {
            write!(f, "{}", self.message)
        } else {
            write!(f, "{}: {}", self.path, self.message)
        }
    }
}

#[cfg(feature = "std")]
impl std::error::Error for DecodeError {}

pub fn decode_path<T: DecodalDecode>(data: &Data, path: &str) -> DecodeResult<T> {
    let value = data_at_path(data, path).ok_or_else(|| DecodeError::new(path, "missing field"))?;
    T::decodal_decode(value).map_err(|error| prefix_error(path, error))
}

pub fn data_at_path<'a>(data: &'a Data, path: &str) -> Option<&'a Data> {
    let mut current = data;
    for part in path.split('.') {
        if part.is_empty() {
            return None;
        }
        let Data::Object(fields) = current else {
            return None;
        };
        current = &fields.iter().find(|field| field.name == part)?.value;
    }
    Some(current)
}

pub fn prefix_decode_error(path: &str, error: DecodeError) -> DecodeError {
    prefix_error(path, error)
}

fn prefix_error(path: &str, mut error: DecodeError) -> DecodeError {
    if error.path.is_empty() {
        error.path = String::from(path);
    } else if !path.is_empty() {
        error.path = format!("{path}.{}", error.path);
    }
    error
}

impl DecodalSchema for String {
    fn decodal_schema() -> HostValue {
        HostValue::string_type()
    }
}

impl DecodalDecode for String {
    fn decodal_decode(data: &Data) -> DecodeResult<Self> {
        match data {
            Data::String(value) => Ok(value.clone()),
            _ => Err(DecodeError::at_type("", "String")),
        }
    }
}

impl IntoHostValue for String {
    fn into_host_value(self) -> HostValue {
        HostValue::string(self)
    }
}

impl IntoHostValue for &str {
    fn into_host_value(self) -> HostValue {
        HostValue::string(self)
    }
}

macro_rules! impl_int_decode {
    ($($ty:ty),* $(,)?) => {
        $(
            impl DecodalSchema for $ty {
                fn decodal_schema() -> HostValue {
                    HostValue::int_type()
                }
            }

            impl DecodalDecode for $ty {
                fn decodal_decode(data: &Data) -> DecodeResult<Self> {
                    match data {
                        Data::Int(value) => <$ty>::try_from(*value)
                            .map_err(|_| DecodeError::new("", "integer value is out of range")),
                        _ => Err(DecodeError::at_type("", "Int")),
                    }
                }
            }

            impl IntoHostValue for $ty {
                fn into_host_value(self) -> HostValue {
                    HostValue::int(self as i64)
                }
            }
        )*
    };
}

impl_int_decode!(i8, i16, i32, i64, u8, u16, u32);

impl DecodalSchema for f64 {
    fn decodal_schema() -> HostValue {
        HostValue::float_type()
    }
}

impl DecodalDecode for f64 {
    fn decodal_decode(data: &Data) -> DecodeResult<Self> {
        match data {
            Data::Float(value) => Ok(*value),
            Data::Int(value) => Ok(*value as f64),
            _ => Err(DecodeError::at_type("", "Float")),
        }
    }
}

impl IntoHostValue for f64 {
    fn into_host_value(self) -> HostValue {
        HostValue::float(self)
    }
}

impl DecodalSchema for f32 {
    fn decodal_schema() -> HostValue {
        HostValue::float_type()
    }
}

impl DecodalDecode for f32 {
    fn decodal_decode(data: &Data) -> DecodeResult<Self> {
        f64::decodal_decode(data).map(|value| value as f32)
    }
}

impl IntoHostValue for f32 {
    fn into_host_value(self) -> HostValue {
        HostValue::float(f64::from(self))
    }
}

impl DecodalSchema for bool {
    fn decodal_schema() -> HostValue {
        HostValue::bool_type()
    }
}

impl DecodalDecode for bool {
    fn decodal_decode(data: &Data) -> DecodeResult<Self> {
        match data {
            Data::Bool(value) => Ok(*value),
            _ => Err(DecodeError::at_type("", "Bool")),
        }
    }
}

impl IntoHostValue for bool {
    fn into_host_value(self) -> HostValue {
        HostValue::bool(self)
    }
}

impl<T> DecodalSchema for Vec<T> {
    fn decodal_schema() -> HostValue {
        HostValue::array_type()
    }
}

impl<T: DecodalDecode> DecodalDecode for Vec<T> {
    fn decodal_decode(data: &Data) -> DecodeResult<Self> {
        match data {
            Data::Array(items) => items
                .iter()
                .enumerate()
                .map(|(index, item)| {
                    T::decodal_decode(item)
                        .map_err(|error| prefix_error(&format!("[{index}]"), error))
                })
                .collect(),
            _ => Err(DecodeError::at_type("", "Array")),
        }
    }
}

impl<T: IntoHostValue> IntoHostValue for Vec<T> {
    fn into_host_value(self) -> HostValue {
        HostValue::array(self.into_iter().map(IntoHostValue::into_host_value))
    }
}
