use serde_json::{Map, Value};

use crate::{error::Error, schema::Schema};

pub struct Bumper;

pub struct BumperOptions {
    pub original: Schema,
    pub kind: BumpKind,
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum BumpKind {
    Xversion,
    Undefined,
}

impl std::str::FromStr for BumpKind {
    type Err = ();

    fn from_str(input: &str) -> Result<BumpKind, Self::Err> {
        match input {
            "x-version" => Ok(BumpKind::Xversion),
            _ => Err(()),
        }
    }
}

impl BumperOptions {
    pub fn with_kind(&mut self, value: BumpKind) -> &mut Self {
        self.kind = value;
        self
    }

    pub fn process(&self, schema: &mut Schema) -> Result<(), Error> {
        let root = schema
            .get_body_mut()
            .as_object_mut()
            .ok_or(Error::InvalidOpenapiSchemaError)?;

        let original = self
            .original
            .get_body()
            .as_object()
            .ok_or(Error::InvalidOpenapiSchemaError)?;

        match self.kind {
            BumpKind::Xversion => {
                let mut bump = (false, false, false);

                let original_info = extract_info(original)?;
                let recent_info = &extract_info_mut(root)?.clone();

                for (property, _) in original_info.into_iter() {
                    if property.starts_with("x-version-") {
                        let original_subversion = extract_version(original_info, property)?;
                        let recent_subversion = extract_version(recent_info, property)?;

                        log::info!(
                            "x: {}, original: {}, recent: {}, ",
                            property,
                            original_subversion,
                            recent_subversion
                        );

                        bump.0 = if original_subversion.major < recent_subversion.major {
                            true
                        } else {
                            bump.0
                        };
                        bump.1 = if original_subversion.minor < recent_subversion.minor {
                            true
                        } else {
                            bump.1
                        };
                        bump.2 = if original_subversion.patch < recent_subversion.patch {
                            true
                        } else {
                            bump.2
                        };
                    }
                }

                let mut original_version = extract_version(original_info, "version")?;
                if bump.0 {
                    original_version.major += 1;
                    original_version.minor = 0;
                    original_version.patch = 0;
                } else if bump.1 {
                    original_version.minor += 1;
                    original_version.patch = 0;
                } else if bump.2 {
                    original_version.patch += 1
                }

                log::info!("bumping version to: {}", original_version);

                let info = extract_info_mut(root)?;
                info.insert(
                    "version".to_string(),
                    Value::String(original_version.to_string()),
                );

                Ok(())
            }
            _ => Err(Error::NotImplemented),
        }
    }
}

impl Bumper {
    pub fn options(original: Schema) -> BumperOptions {
        BumperOptions {
            original,
            kind: BumpKind::Xversion,
        }
    }
}

fn extract_info(openapi: &Map<String, Value>) -> Result<&Map<String, Value>, Error> {
    openapi
        .get("info")
        .ok_or(Error::InvalidOpenapiSchemaError)?
        .as_object()
        .ok_or(Error::InvalidOpenapiSchemaError)
}

fn extract_info_mut(openapi: &mut Map<String, Value>) -> Result<&mut Map<String, Value>, Error> {
    openapi
        .get_mut("info")
        .ok_or(Error::InvalidOpenapiSchemaError)?
        .as_object_mut()
        .ok_or(Error::InvalidOpenapiSchemaError)
}

#[cfg(feature = "semver")]
fn extract_version(info: &Map<String, Value>, field_name: &str) -> Result<semver::Version, Error> {
    semver::Version::parse(
        info.get(field_name)
            .ok_or(Error::InvalidOpenapiSchemaError)?
            .as_str()
            .ok_or(Error::InvalidOpenapiSchemaError)?,
    )
    .map_err(Error::SemVersion)
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn test_xversion_bump_major() {
        let recent = json!({
            "info": {
                "version": "0.0.8",
                "x-version-service1": "0.0.1",
                "x-version-service2": "1.0.0",
            }
        });

        let original = json!({
            "info": {
                "version": "0.0.8",
                "x-version-service1": "0.0.1",
                "x-version-service2": "0.5.0",
            }
        });

        let expected = json!({
            "info": {
                "version": "1.0.0",
                "x-version-service1": "0.0.1",
                "x-version-service2": "1.0.0",
            }
        });

        let mut schema = Schema::from_json(recent);

        let _result = Bumper::options(Schema::from_json(original))
            .with_kind(BumpKind::Xversion)
            .process(&mut schema);

        assert_eq!(schema.get_body().to_string(), expected.to_string());
    }

    #[test]
    fn test_xversion_bump_patch() {
        let recent = json!({
            "info": {
                "version": "0.0.8",
                "x-version-service1": "0.0.1",
                "x-version-service2": "0.5.1",
            }
        });

        let original = json!({
            "info": {
                "version": "0.0.8",
                "x-version-service1": "0.0.1",
                "x-version-service2": "0.5.0",
            }
        });

        let expected = json!({
            "info": {
                "version": "0.0.9",
                "x-version-service1": "0.0.1",
                "x-version-service2": "0.5.1",
            }
        });

        let mut schema = Schema::from_json(recent);

        let _result = Bumper::options(Schema::from_json(original))
            .with_kind(BumpKind::Xversion)
            .process(&mut schema);

        assert_eq!(schema.get_body().to_string(), expected.to_string());
    }

    #[test]
    fn test_xversion_bump_minor() {
        let recent = json!({
            "info": {
                "version": "0.0.8",
                "x-version-service1": "0.0.1",
                "x-version-service2": "0.6.0",
            }
        });

        let original = json!({
            "info": {
                "version": "0.0.8",
                "x-version-service1": "0.0.1",
                "x-version-service2": "0.5.0",
            }
        });

        let expected = json!({
            "info": {
                "version": "0.1.0",
                "x-version-service1": "0.0.1",
                "x-version-service2": "0.6.0",
            }
        });

        let mut schema = Schema::from_json(recent);

        let _result = Bumper::options(Schema::from_json(original))
            .with_kind(BumpKind::Xversion)
            .process(&mut schema);

        assert_eq!(schema.get_body().to_string(), expected.to_string());
    }
}
