use std::{collections::HashSet, fmt, hash::Hash};

use itertools::Itertools;
use rmpv::{Utf8StringRef, Value, ValueRef};

#[cfg(test)]
use crate::bridge::nvim_dict;

#[derive(Debug, Clone)]
pub struct ApiInfoParseError(String);

impl std::error::Error for ApiInfoParseError {}

impl fmt::Display for ApiInfoParseError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl From<ValueRef<'_>> for ApiInfoParseError {
    fn from(value: ValueRef) -> Self {
        Self(format!("{value}"))
    }
}

impl From<&str> for ApiInfoParseError {
    fn from(value: &str) -> Self {
        Self(value.to_owned())
    }
}

#[allow(unused)]
#[derive(Debug)]
pub struct ApiVersion {
    pub major: u64,
    pub minor: u64,
    pub patch: u64,
    pub prerelease: bool,
    pub api_level: u64,
    pub api_compatible: u64,
    pub api_prerelease: bool,
    pub prerelease_version: u64,
    pub prerelease_commit: String,
    pub string: String,
}

impl ApiVersion {
    /// A pre-release only matches lower versions when prerelease is set to None.
    /// So NVIM v0.12.0-dev-1253+gfb2d736481 matches for the arguments 0, 11, 1, None but not for 0, 12, 0, None.
    /// When the prerelease is set to some number it compares the field after -dev in the version string.
    /// So the above version matches for the arguments 0, 12, 0, Some(1253), but not for 0, 12, 0, Some(1254).
    /// See the tests for more examples.
    #[allow(dead_code)]
    pub fn has_version(&self, major: u64, minor: u64, patch: u64, prerelease: Option<u64>) -> bool {
        self.major > major
            || (self.major == major && self.minor > minor)
            || ((self.major == major && self.minor == minor && self.patch >= patch)
                && !self.prerelease)
            || (self.major == major
                && self.minor == minor
                && self.patch == patch
                && matches!(prerelease, Some(prerelease) if self.prerelease_version >= prerelease))
    }
}

#[allow(unused)]
#[derive(Debug)]
pub struct ApiFunction {
    pub name: String,
    pub parameters: Vec<ApiParameter>,
    pub return_type: Option<ApiParameterType>,
    pub method: Option<bool>,
    pub since: u64,
    pub deprecated_since: Option<u64>,
}

impl Hash for ApiFunction {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        self.name.hash(state)
    }
}

impl PartialEq for ApiFunction {
    fn eq(&self, other: &Self) -> bool {
        self.name == other.name
    }
}

impl Eq for ApiFunction {}

#[allow(unused)]
#[derive(Debug)]
pub enum ApiParameterType {
    Nil,
    Boolean,
    Integer,
    Float,
    String,
    Array,
    Dictionary,
    Object,
    Buffer,
    Window,
    Tabpage,
    ArrayOf(Box<ApiParameterType>),
    SizedArrayOf(Box<ApiParameterType>, usize),
    LuaRef,
    Void,
    Unknown(String),
}

impl ApiParameterType {
    fn new(value: Option<&str>) -> Self {
        match value {
            Some("Nil") => ApiParameterType::Nil,
            Some("Boolean") => ApiParameterType::Boolean,
            Some("Integer") => ApiParameterType::Integer,
            Some("Float") => ApiParameterType::Float,
            Some("String") => ApiParameterType::String,
            Some("Array") => ApiParameterType::Array,
            Some("Dictionary") => ApiParameterType::Dictionary,
            Some("Object") => ApiParameterType::Object,
            Some("Buffer") => ApiParameterType::Buffer,
            Some("Window") => ApiParameterType::Window,
            Some("Tabpage") => ApiParameterType::Tabpage,
            Some("LuaRef") => ApiParameterType::LuaRef,
            Some("void") => ApiParameterType::Void,
            Some(unknown) => {
                if let Some(array_of) = unknown.strip_prefix("ArrayOf(") {
                    let array_of = array_of.strip_suffix(')').unwrap();
                    let mut parts = array_of.split(',');
                    if let Some(name) = parts.next() {
                        let name = Box::new(Self::new(Some(name.trim())));
                        if let Some(s) = parts.next() {
                            let size = s.trim().parse::<usize>().unwrap_or(0);
                            return ApiParameterType::SizedArrayOf(name, size);
                        } else {
                            return ApiParameterType::ArrayOf(name);
                        }
                    }
                }
                ApiParameterType::Unknown(unknown.to_owned())
            }
            None => ApiParameterType::Unknown("".to_owned()),
        }
    }
}

#[allow(unused)]
#[derive(Debug)]
pub struct ApiParameter {
    pub name: String,
    pub parameter_type: ApiParameterType,
}

#[allow(unused)]
#[derive(Debug)]
pub struct ApiEvent {
    pub name: String,
    pub parameters: Vec<ApiParameter>,
    pub since: u64,
}

impl Hash for ApiEvent {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        self.name.hash(state)
    }
}

impl PartialEq for ApiEvent {
    fn eq(&self, other: &Self) -> bool {
        self.name == other.name
    }
}

impl Eq for ApiEvent {}

#[allow(unused)]
#[derive(Debug)]
pub struct ApiInformation {
    pub channel: u64,
    pub version: ApiVersion,
    pub functions: HashSet<ApiFunction>,
    pub ui_options: Vec<String>,
    pub ui_events: HashSet<ApiEvent>,
    // types and error_error types are not implemented
}

impl ApiInformation {
    #[allow(dead_code)]
    pub fn has_event(&self, event_name: &str) -> bool {
        self.ui_events.iter().any(|event| event.name == event_name)
    }

    #[allow(dead_code)]
    pub fn has_function(&self, name: &str) -> bool {
        self.functions.iter().any(|func| func.name == name)
    }
}

fn parse_version(
    value: ValueRef,
    version_str: &str,
) -> std::result::Result<ApiVersion, ApiInfoParseError> {
    let mut major = None;
    let mut minor = None;
    let mut patch = None;
    let mut prerelease = None;
    let mut api_level = None;
    let mut api_compatible = None;
    let mut api_prerelase = None;

    let version: Vec<(ValueRef, ValueRef)> = value.try_into()?;
    for (k, v) in version {
        let k: Utf8StringRef = k.try_into()?;
        match k.as_str() {
            Some("major") => major = Some(v.try_into()?),
            Some("minor") => minor = Some(v.try_into()?),
            Some("patch") => patch = Some(v.try_into()?),
            Some("prerelease") => prerelease = Some(v.try_into()?),
            Some("api_level") => api_level = Some(v.try_into()?),
            Some("api_compatible") => api_compatible = Some(v.try_into()?),
            // api_prerelease should be a boolean value, but Neovim 0.10.0 sets it to nil for some reason, so assume nil means release
            Some("api_prerelease") => api_prerelase = Some(!v.to_owned().is_nil() && v.try_into()?),
            _ => {}
        }
    }

    // The api information does unfortunately not contain the detailed git information, so parse that from the version string
    // A pre-release has the following format
    // NVIM v0.12.0-dev-1253+gfb2d736481
    let mut prerelease_version = 0;
    let mut prerelease_commit = String::default();
    if let Some((_, dev, version)) =
        version_str.strip_suffix("-dirty").unwrap_or(version_str).split('-').collect_tuple()
    {
        if dev == "dev" {
            if let Some((version, commit)) = version.split("+").collect_tuple() {
                prerelease_version = version.parse().unwrap_or(0);
                prerelease_commit = commit.to_string();
            }
        }
    }

    Ok(ApiVersion {
        major: major.ok_or("major field is missing")?,
        minor: minor.ok_or("minor field is missing")?,
        patch: patch.ok_or("patch field is missing")?,
        prerelease: prerelease.ok_or("prerelease field is isssing")?,
        api_level: api_level.ok_or("api_level field is missing")?,
        api_compatible: api_compatible.ok_or("api_compatible field is missing")?,
        api_prerelease: api_prerelase.ok_or("api_prerelease field is missing")?,
        prerelease_version,
        prerelease_commit,
        string: version_str.to_string(),
    })
}

fn parse_function(value: ValueRef) -> std::result::Result<ApiFunction, ApiInfoParseError> {
    let mut name = None;
    let mut parameters = None;
    let mut return_type = None;
    let mut method = None;
    let mut since = None;
    let mut deprecated_since = None;
    let fields: Vec<(ValueRef, ValueRef)> = value.try_into()?;
    for (key, v) in fields {
        let k: Utf8StringRef = key.try_into()?;
        match k.as_str() {
            Some("name") => {
                let n: Utf8StringRef = v.try_into()?;
                name = n.as_str().map(|n| n.to_owned())
            }
            Some("parameters") => parameters = Some(parse_parameters(v)?),
            Some("return_type") => return_type = Some(parse_parameter_type(v)?),
            Some("method") => method = Some(v.try_into()?),
            Some("since") => since = Some(v.try_into()?),
            Some("deprecated_since") => deprecated_since = Some(v.try_into()?),
            Some(key) => return Err(key.into()),
            _ => {}
        }
    }
    Ok(ApiFunction {
        name: name.ok_or("name field is missing")?,
        parameters: parameters.ok_or("parameters field is missing")?,
        return_type,
        method,
        since: since.ok_or("since field is missing")?,
        deprecated_since,
    })
}

fn parse_functions(
    value: ValueRef,
) -> std::result::Result<HashSet<ApiFunction>, ApiInfoParseError> {
    let functions: Vec<ValueRef> = value.try_into()?;
    functions.into_iter().map(parse_function).collect::<std::result::Result<HashSet<_>, _>>()
}

fn parse_parameter_type(
    value: ValueRef,
) -> std::result::Result<ApiParameterType, ApiInfoParseError> {
    let parameter_type: Utf8StringRef = value.try_into()?;
    Ok(ApiParameterType::new(parameter_type.as_str()))
}

fn parse_parameter(value: ValueRef) -> std::result::Result<ApiParameter, ApiInfoParseError> {
    let info: Vec<ValueRef> = value.try_into()?;
    if let Some((t, n)) = info.into_iter().collect_tuple() {
        let name: Utf8StringRef = n.try_into()?;
        let name = name.as_str();
        let parameter_type = parse_parameter_type(t)?;
        Ok(ApiParameter {
            name: name.map_or(Err("name field is missing"), |v| Ok(v.to_owned()))?,
            parameter_type,
        })
    } else {
        Err("Invalid parameter".into())
    }
}

fn parse_parameters(value: ValueRef) -> std::result::Result<Vec<ApiParameter>, ApiInfoParseError> {
    let parameters: Vec<ValueRef> = value.try_into()?;
    parameters.into_iter().map(parse_parameter).collect::<std::result::Result<Vec<_>, _>>()
}

fn parse_string(value: ValueRef) -> std::result::Result<String, ApiInfoParseError> {
    let value: Utf8StringRef = value.try_into()?;
    value
        .as_str()
        .map(|value| value.to_owned())
        .ok_or_else(|| "Failed to parse string value".into())
}

fn parse_string_vec(value: ValueRef) -> std::result::Result<Vec<String>, ApiInfoParseError> {
    let options: Vec<ValueRef> = value.try_into()?;
    options.into_iter().map(parse_string).collect::<std::result::Result<Vec<_>, _>>()
}

fn parse_ui_event(value: ValueRef) -> std::result::Result<ApiEvent, ApiInfoParseError> {
    let mut name = None;
    let mut parameters = None;
    let mut since = None;
    let fields: Vec<(ValueRef, ValueRef)> = value.try_into()?;
    for (key, v) in fields {
        let k: Utf8StringRef = key.try_into()?;
        match k.as_str() {
            Some("name") => {
                let n: Utf8StringRef = v.try_into()?;
                name = n.as_str().map(|n| n.to_owned())
            }
            Some("parameters") => parameters = Some(parse_parameters(v)?),
            Some("since") => since = Some(v.try_into()?),
            Some(key) => return Err(key.into()),
            _ => {}
        }
    }
    Ok(ApiEvent {
        name: name.ok_or("name field is missing")?,
        parameters: parameters.ok_or("parameters field is missing")?,
        since: since.ok_or("since field is missing")?,
    })
}

fn parse_ui_events(value: ValueRef) -> std::result::Result<HashSet<ApiEvent>, ApiInfoParseError> {
    let functions: Vec<ValueRef> = value.try_into()?;
    functions.into_iter().map(parse_ui_event).collect::<std::result::Result<HashSet<_>, _>>()
}

pub fn parse_api_info(
    value: &[Value],
    version_str: &str,
) -> std::result::Result<ApiInformation, ApiInfoParseError> {
    let channel = value[0].as_ref().try_into()?;

    let metadata: Vec<(ValueRef, ValueRef)> = value[1].as_ref().try_into()?;

    let mut version = None;
    let mut functions = None;
    let mut ui_options = None;
    let mut ui_events = None;

    for (k, v) in metadata {
        let k: Utf8StringRef = k.try_into()?;
        match k.as_str() {
            Some("version") => version = Some(parse_version(v, version_str)?),
            Some("functions") => functions = Some(parse_functions(v)?),
            Some("ui_options") => ui_options = Some(parse_string_vec(v)?),
            Some("ui_events") => ui_events = Some(parse_ui_events(v)?),
            _ => {}
        }
    }

    Ok(ApiInformation {
        channel,
        version: version.ok_or("version field is missing")?,
        functions: functions.ok_or("functions field is missing")?,
        ui_options: ui_options.ok_or("ui_options field is missing")?,
        ui_events: ui_events.ok_or("ui_events field is missing")?,
    })
}

#[test]
fn version_match() {
    let value = nvim_dict! {
        "major" => 1,
        "minor" => 11,
        "patch" => 4,
        "prerelease" => false,
        "api_level" => 0,
        "api_compatible" => 0,
        "api_prerelease" => false,
    };

    let version = parse_version(Value::from(value).as_ref(), "NVIM v1.11.4").unwrap();
    assert!(version.has_version(1, 11, 4, None));
    assert!(version.has_version(1, 11, 3, None));
    assert!(version.has_version(1, 10, 0, None));
    assert!(version.has_version(0, 11, 4, None));
    // We also have a pre-release of previous versions
    assert!(version.has_version(1, 11, 4, Some(1253)));
    assert!(version.has_version(1, 10, 4, Some(1253)));
    assert!(!version.has_version(1, 11, 5, None));
    assert!(!version.has_version(1, 12, 4, None));
    assert!(!version.has_version(2, 11, 4, None));
    // We don't have a pre-release of newer versions
    assert!(!version.has_version(1, 12, 0, Some(0)));
    assert!(!version.has_version(1, 11, 5, Some(0)));
}

#[test]
fn version_match_prerelease() {
    let value = nvim_dict! {
        "major" => 1,
        "minor" => 12,
        "patch" => 0,
        "prerelease" => true,
        "api_level" => 0,
        "api_compatible" => 0,
        "api_prerelease" => false,
    };

    let version =
        parse_version(Value::from(value).as_ref(), "NVIM v1.12.0-dev-1253+gfb2d736481").unwrap();
    assert!(version.has_version(1, 11, 4, None));
    assert!(version.has_version(1, 11, 3, None));
    assert!(version.has_version(1, 10, 0, None));
    assert!(version.has_version(0, 11, 4, None));
    // We also have a pre-release of previous versions
    assert!(version.has_version(1, 11, 4, Some(1253)));
    assert!(version.has_version(1, 10, 4, Some(1253)));
    assert!(version.has_version(1, 11, 5, None));
    assert!(!version.has_version(1, 12, 4, None));
    assert!(!version.has_version(2, 11, 4, None));
    // We have a pre-release that allows these
    assert!(version.has_version(1, 12, 0, Some(0)));
    assert!(version.has_version(1, 12, 0, Some(1253)));
    assert!(version.has_version(1, 11, 5, Some(0)));
    // But not these
    assert!(!version.has_version(1, 12, 1, Some(0)));
    assert!(!version.has_version(1, 12, 0, Some(1254)));
}
