use anyhow::Result;
use anyhow::bail;
use dprint_core::configuration::ConfigKeyMap;
use dprint_core::configuration::ConfigKeyValue;
use jsonc_parser::JsonArray;
use jsonc_parser::JsonObject;
use jsonc_parser::JsonValue;

use super::ConfigMap;
use super::ConfigMapValue;
use super::RawPluginConfig;
use super::RawPluginConfigOverride;

pub fn deserialize_config(config_file_text: &str) -> Result<ConfigMap> {
  let value = jsonc_parser::parse_to_value(config_file_text, &Default::default())?;

  let root_object_node = match value {
    Some(JsonValue::Object(obj)) => obj,
    _ => return Ok(Default::default()),
  };

  let mut properties = ConfigMap::new();

  for (key, value) in root_object_node.into_iter() {
    let property_name = key;
    let property_value = match value {
      JsonValue::Object(obj) => ConfigMapValue::PluginConfig(json_obj_to_raw_plugin_config(&property_name, obj)?),
      JsonValue::Array(arr) => ConfigMapValue::Vec(json_array_to_vec(&property_name, arr)?),
      JsonValue::Boolean(value) => ConfigMapValue::from_bool(value),
      JsonValue::String(value) => ConfigMapValue::KeyValue(ConfigKeyValue::String(value.into_owned())),
      JsonValue::Number(value) => ConfigMapValue::from_i32(match value.parse::<i32>() {
        Ok(value) => value,
        Err(err) => {
          bail!(
            "Expected property '{}' with value '{}' to be convertible to a signed integer. {}",
            property_name,
            value,
            err
          )
        }
      }),
      JsonValue::Null => bail!("Unexpected null value in root object property '{}'", property_name), // ignore
    };
    properties.insert(property_name, property_value);
  }

  Ok(properties)
}

pub fn deserialize_config_raw(config_file_text: &str) -> Result<ConfigKeyMap> {
  let value = jsonc_parser::parse_to_value(config_file_text, &Default::default())?;
  let root_object_node = match value {
    Some(JsonValue::Object(obj)) => obj,
    _ => return Ok(Default::default()),
  };

  object_to_config_key_map(root_object_node)
}

fn json_obj_to_raw_plugin_config(parent_prop_name: &str, obj: JsonObject) -> Result<RawPluginConfig> {
  let mut properties = ConfigKeyMap::new();
  let mut locked = false;
  let mut associations = None;
  let mut overrides = Vec::new();

  for (key, value) in obj.into_iter() {
    let property_name = key;
    if property_name == "locked" {
      match value {
        JsonValue::Boolean(value) => {
          locked = value;
          continue;
        }
        _ => bail!("The 'locked' property in a plugin configuration must be a boolean."),
      }
    }

    if property_name == "associations" {
      match value {
        JsonValue::Array(value) => {
          let mut items = Vec::new();
          for value in value.into_iter() {
            match value {
              JsonValue::String(value) => items.push(value.into_owned()),
              _ => bail!("The 'associations' array in a plugin configuration must contain only strings."),
            }
          }
          associations = Some(items);
          continue;
        }
        JsonValue::String(value) => {
          associations = Some(vec![value.into_owned()]);
          continue;
        }
        _ => bail!("The 'associations' property in a plugin configuration must be a string or an array of strings."),
      }
    }

    if property_name == "overrides" {
      overrides = match value {
        JsonValue::Object(value) => vec![json_obj_to_raw_plugin_config_override(value)?],
        JsonValue::Array(value) => {
          let mut items = Vec::new();
          for value in value.into_iter() {
            match value {
              JsonValue::Object(value) => items.push(json_obj_to_raw_plugin_config_override(value)?),
              _ => bail!("The 'overrides' property in a plugin configuration must be an object or an array of objects."),
            }
          }
          items
        }
        _ => bail!("The 'overrides' property in a plugin configuration must be an object or an array of objects."),
      };
      continue;
    }

    let property_value = match value_to_plugin_config_key_value(value) {
      Ok(result) => result,
      Err(err) => bail!("{} in object property '{} -> {}'", err, parent_prop_name, property_name),
    };
    properties.insert(property_name, property_value);
  }

  Ok(RawPluginConfig {
    locked,
    associations,
    overrides,
    properties,
  })
}

fn json_obj_to_raw_plugin_config_override(obj: JsonObject) -> Result<RawPluginConfigOverride> {
  let mut files = None;
  let mut properties = ConfigKeyMap::new();

  for (key, value) in obj.into_iter() {
    if key == "files" {
      files = Some(match value {
        JsonValue::Array(value) => {
          let mut items = Vec::new();
          for value in value.into_iter() {
            match value {
              JsonValue::String(value) => items.push(value.into_owned()),
              _ => bail!("The 'files' array in a plugin configuration override must contain only strings."),
            }
          }
          items
        }
        JsonValue::String(value) => vec![value.into_owned()],
        _ => bail!("The 'files' property in a plugin configuration override must be a string or an array of strings."),
      });
    } else {
      let property_value = match value_to_plugin_config_key_value(value) {
        Ok(result) => result,
        Err(err) => bail!("{} in plugin configuration override property '{}'", err, key),
      };
      properties.insert(key, property_value);
    }
  }

  let files = match files {
    Some(files) => files,
    None => bail!("A plugin configuration override must specify a 'files' property."),
  };
  if files.is_empty() {
    bail!("A plugin configuration override must specify at least one file pattern.");
  }
  if properties.is_empty() {
    bail!("A plugin configuration override must specify at least one configuration property.");
  }

  Ok(RawPluginConfigOverride { files, properties })
}

fn json_array_to_vec(parent_prop_name: &str, array: JsonArray) -> Result<Vec<String>> {
  let mut elements = Vec::new();

  for element in array.into_iter() {
    let value = match value_to_string(element) {
      Ok(result) => result,
      Err(err) => bail!("{} in array '{}'", err, parent_prop_name),
    };
    elements.push(value);
  }

  Ok(elements)
}

fn value_to_string(value: JsonValue) -> Result<String> {
  match value {
    JsonValue::String(value) => Ok(value.into_owned()),
    _ => bail!("Expected a string"),
  }
}

fn value_to_plugin_config_key_value(value: JsonValue) -> Result<ConfigKeyValue> {
  Ok(match value {
    JsonValue::Boolean(value) => ConfigKeyValue::Bool(value),
    JsonValue::String(value) => ConfigKeyValue::String(value.into_owned()),
    JsonValue::Number(value) => ConfigKeyValue::Number(value.parse::<i32>()?),
    JsonValue::Array(value) => {
      let values = value
        .into_iter()
        .map(value_to_plugin_config_key_value)
        .collect::<Result<Vec<ConfigKeyValue>, _>>()?;
      ConfigKeyValue::Array(values)
    }
    JsonValue::Object(obj) => ConfigKeyValue::Object(object_to_config_key_map(obj)?),
    JsonValue::Null => ConfigKeyValue::Null,
  })
}

fn object_to_config_key_map(obj: JsonObject) -> Result<ConfigKeyMap> {
  let mut properties = ConfigKeyMap::new();
  for (key, value) in obj.into_iter() {
    let value = match value_to_plugin_config_key_value(value) {
      Ok(result) => result,
      Err(err) => bail!("{} in object property '{}'", err, key),
    };
    properties.insert(key, value);
  }
  Ok(properties)
}

#[cfg(test)]
mod tests {
  use super::deserialize_config;
  use crate::configuration::ConfigMap;
  use crate::configuration::ConfigMapValue;
  use crate::configuration::RawPluginConfig;
  use crate::configuration::RawPluginConfigOverride;

  use dprint_core::configuration::ConfigKeyMap;
  use dprint_core::configuration::ConfigKeyValue;
  use pretty_assertions::assert_eq;

  #[test]
  fn should_error_when_there_is_a_parser_error() {
    assert_error("{prop}", "Unexpected token on line 1 column 2");
  }

  #[test]
  fn should_not_error_when_no_object_in_root() {
    assert_deserializes("", ConfigMap::new());
    assert_deserializes("[]", ConfigMap::new());
  }

  #[test]
  fn should_error_when_the_root_property_has_an_unexpected_value_type() {
    assert_error("{'prop': null}", "Unexpected null value in root object property 'prop'");
  }

  #[test]
  fn should_deserialize_empty_object() {
    assert_deserializes("{}", ConfigMap::new());
  }

  #[test]
  fn should_deserialize_full_object() {
    let mut expected_props = ConfigMap::new();
    expected_props.insert(String::from("includes"), ConfigMapValue::Vec(Vec::new()));
    expected_props.insert(
      String::from("typescript"),
      ConfigMapValue::PluginConfig(RawPluginConfig {
        locked: false,
        associations: None,
        overrides: Vec::new(),
        properties: ConfigKeyMap::from([
          (String::from("lineWidth"), ConfigKeyValue::from_i32(40)),
          (String::from("preferSingleLine"), ConfigKeyValue::from_bool(true)),
          (String::from("other"), ConfigKeyValue::from_str("test")),
          (
            String::from("obj"),
            ConfigKeyValue::Object(ConfigKeyMap::from([(String::from("prop"), ConfigKeyValue::from_i32(5))])),
          ),
          (
            String::from("array"),
            ConfigKeyValue::Array(vec![ConfigKeyValue::from_i32(1), ConfigKeyValue::Null]),
          ),
        ]),
      }),
    );
    assert_deserializes(
      "{'includes': [], 'typescript': { 'lineWidth': 40, 'preferSingleLine': true, 'other': 'test', 'obj': { 'prop': 5 }, 'array': [1, null] }}",
      expected_props,
    );
  }

  #[test]
  fn should_deserialize_cli_specific_plugin_config() {
    let expected_props = ConfigMap::from([
      (
        "typescript".to_string(),
        ConfigMapValue::PluginConfig(RawPluginConfig {
          locked: true,
          associations: Some(vec!["test".to_string()]),
          overrides: Vec::new(),
          properties: ConfigKeyMap::from([("lineWidth".to_string(), ConfigKeyValue::from_i32(40))]),
        }),
      ),
      (
        "other".to_string(),
        ConfigMapValue::PluginConfig(RawPluginConfig {
          locked: false,
          associations: Some(vec!["other".to_string(), "test".to_string()]),
          overrides: Vec::new(),
          properties: ConfigKeyMap::new(),
        }),
      ),
    ]);
    assert_deserializes(
      "{'typescript': { 'lineWidth': 40, locked: true, associations: 'test' }, 'other': { 'locked': false, 'associations': ['other', 'test'] }}",
      expected_props,
    );
  }

  #[test]
  fn error_invalid_cli_specific_properties() {
    assert_error(
      "{'typescript': { 'associations': [1] }}",
      "The 'associations' array in a plugin configuration must contain only strings.",
    );
    assert_error(
      "{'typescript': { 'associations': 1 }}",
      "The 'associations' property in a plugin configuration must be a string or an array of strings.",
    );
    assert_error(
      "{'typescript': { locked: 1 }}",
      "The 'locked' property in a plugin configuration must be a boolean.",
    );
  }

  #[test]
  fn should_deserialize_plugin_config_overrides() {
    let expected_props = ConfigMap::from([(
      "typescript".to_string(),
      ConfigMapValue::PluginConfig(RawPluginConfig {
        locked: false,
        associations: None,
        overrides: vec![RawPluginConfigOverride {
          files: vec!["**/package.json".to_string(), "**/composer.json".to_string()],
          properties: ConfigKeyMap::from([
            ("indentWidth".to_string(), ConfigKeyValue::from_i32(4)),
            ("useTabs".to_string(), ConfigKeyValue::from_bool(false)),
          ]),
        }],
        properties: ConfigKeyMap::from([("lineWidth".to_string(), ConfigKeyValue::from_i32(80))]),
      }),
    )]);

    assert_deserializes(
      "{'typescript': { 'lineWidth': 80, 'overrides': { 'files': ['**/package.json', '**/composer.json'], 'indentWidth': 4, 'useTabs': false } }}",
      expected_props,
    );
  }

  #[test]
  fn should_deserialize_plugin_config_overrides_array() {
    let expected_props = ConfigMap::from([(
      "typescript".to_string(),
      ConfigMapValue::PluginConfig(RawPluginConfig {
        locked: false,
        associations: None,
        overrides: vec![
          RawPluginConfigOverride {
            files: vec!["**/package.json".to_string()],
            properties: ConfigKeyMap::from([("indentWidth".to_string(), ConfigKeyValue::from_i32(4))]),
          },
          RawPluginConfigOverride {
            files: vec!["**/special-package.json".to_string()],
            properties: ConfigKeyMap::from([("lineWidth".to_string(), ConfigKeyValue::from_i32(80))]),
          },
        ],
        properties: ConfigKeyMap::new(),
      }),
    )]);

    assert_deserializes(
      "{'typescript': { 'overrides': [{ 'files': '**/package.json', 'indentWidth': 4 }, { 'files': ['**/special-package.json'], 'lineWidth': 80 }] }}",
      expected_props,
    );
  }

  #[test]
  fn error_invalid_plugin_config_overrides() {
    assert_error(
      "{'typescript': { 'overrides': 5 }}",
      "The 'overrides' property in a plugin configuration must be an object or an array of objects.",
    );
    assert_error(
      "{'typescript': { 'overrides': [{ 'files': [1], 'indentWidth': 4 }] }}",
      "The 'files' array in a plugin configuration override must contain only strings.",
    );
    assert_error(
      "{'typescript': { 'overrides': [{ 'files': 5, 'indentWidth': 4 }] }}",
      "The 'files' property in a plugin configuration override must be a string or an array of strings.",
    );
    assert_error(
      "{'typescript': { 'overrides': [{ 'indentWidth': 4 }] }}",
      "A plugin configuration override must specify a 'files' property.",
    );
    assert_error(
      "{'typescript': { 'overrides': [{ 'files': [], 'indentWidth': 4 }] }}",
      "A plugin configuration override must specify at least one file pattern.",
    );
    assert_error(
      "{'typescript': { 'overrides': [{ 'files': '**/package.json' }] }}",
      "A plugin configuration override must specify at least one configuration property.",
    );
  }

  #[test]
  fn should_have_stable_deserialization_of_config_properties() {
    for _ in 0..10 {
      let config = deserialize_config(
        r#"{
        "exec": {
          "commands": [{
            "command": "rustfmt --edition 2024 --config imports_granularity=item",
            "exts": ["rs"]
          }]
        }
      }"#,
      )
      .unwrap();
      match config.get("exec").unwrap() {
        ConfigMapValue::PluginConfig(plugin) => {
          let commands = plugin.properties.get("commands").unwrap().as_array().unwrap();
          assert_eq!(commands.len(), 1);
          let obj = commands[0].as_object().unwrap();
          let mut values = obj.into_iter();
          assert_eq!(values.next().unwrap().0, "command");
          assert_eq!(values.next().unwrap().0, "exts");
          assert!(values.next().is_none());
        }
        _ => unreachable!(),
      }
    }
  }

  fn assert_deserializes(text: &str, expected_map: ConfigMap) {
    match deserialize_config(text) {
      Ok(result) => assert_eq!(result, expected_map),
      Err(err) => panic!("Errored, but that was not expected. {:#}", err),
    }
  }

  fn assert_error(text: &str, expected_err: &str) {
    match deserialize_config(text) {
      Ok(_) => panic!("Did not error, but that was expected."),
      Err(err) => assert_eq!(err.to_string(), expected_err),
    }
  }
}
