use jsonpath_lib::replace_with;
use jsonpath_rust::JsonPath;

use {
    icann_rdap_common::response::redacted::Redacted,
    serde_json::{Value, json},
};

use {
    super::{MdParams, ToMd, string::StringUtil, table::MultiPartTable},
    icann_rdap_common::response::RdapResponse,
};

/// The text to appear if something is redacted.
///
/// This should be REDACTED in bold.
pub const REDACTED_TEXT: &str = "*REDACTED*";

impl ToMd for &[Redacted] {
    fn to_md(&self, params: MdParams) -> String {
        let mut md = String::new();

        // header
        let header_text = "Redacted".to_string();
        md.push_str(&header_text.to_header(params.heading_level, params.options));

        // multipart data
        let mut table = MultiPartTable::new();
        // table = table.header_ref(&"Fields");

        for (index, redacted) in self.iter().enumerate() {
            if index > 0 {
                table = table.add_separator();
            }

            // reason data
            let reason_type = redacted.reason().and_then(|r| r.type_field());
            let reason_description = redacted.reason().and_then(|r| r.description());

            // Special case the 'column' fields
            table = table
                .and_nv_ref_maybe(&"Type", &redacted.name.type_field())
                .and_nv_ref_maybe(&"Description", &redacted.name.description())
                .and_nv_ref_maybe(&"Pre Path", &redacted.pre_path())
                .and_nv_ref_maybe(&"Post Path", &redacted.post_path())
                .and_nv_ref_maybe(&"Replacement Path", &redacted.replacement_path())
                .and_nv_ref_maybe(&"Path Language", &redacted.path_lang())
                .and_nv_ref_maybe(&"Method", &redacted.method())
                .and_nv_ref_maybe(&"Reason Type", &reason_type)
                .and_nv_ref_maybe(&"Reason Description", &reason_description);
        }

        // render table
        md.push_str(&table.to_md(params));
        md.push('\n');
        md
    }
}

// this is our public entry point
pub fn replace_redacted_items(orignal_response: RdapResponse) -> RdapResponse {
    // convert the RdapResponse to a string
    let rdap_json = serde_json::to_string(&orignal_response).unwrap();

    // Redaction is not a top-level entity so we have to check the JSON
    // to see if anything exists in the way of "redacted", this should find it in the rdapConformance
    if !rdap_json.contains("\"redacted\"") {
        // If there are no redactions, return the original response
        return orignal_response;
    }

    // convert the string to a JSON Value
    let mut rdap_json_response: Value = serde_json::from_str(&rdap_json).unwrap();

    // this double checks to see if "redacted" is an array
    if rdap_json_response["redacted"].as_array().is_none() {
        // If "redacted" is not an array, return the original response
        return orignal_response;
    }

    // Initialize the final response with the original response
    let mut response = orignal_response;
    // pull the redacted array out of the JSON
    let redacted_array_option = rdap_json_response["redacted"].as_array().cloned();

    // if there are any redactions we need to do some modifications
    if let Some(ref redacted_array) = redacted_array_option {
        let new_json_response = convert_redactions(&mut rdap_json_response, redacted_array).clone();
        // convert the Value back to a RdapResponse
        response = serde_json::from_value(new_json_response).unwrap();
    }

    // send the response back so we can display it to the client
    response
}

fn convert_redactions<'a>(
    rdap_json_response: &'a mut Value,
    redacted_array: &'a [Value],
) -> &'a mut Value {
    for item in redacted_array {
        let item_map = item.as_object().unwrap();
        let post_path = get_string_from_map(item_map, "postPath");
        let method = get_string_from_map(item_map, "method");

        if let Some(path_lang) = item_map.get("pathLang")
            && let Some(path_lang) = path_lang.as_str()
            && !path_lang.eq_ignore_ascii_case("jsonpath")
        {
            continue;
        }

        // if method doesn't equal emptyValue or partialValue, we don't need to do anything, we can skip to the next item
        if method != "emptyValue" && method != "partialValue" && !post_path.is_empty() {
            continue;
        }

        match rdap_json_response.clone().query_with_path(&post_path) {
            Ok(mut paths) => {
                for path_ref in paths.iter_mut() {
                    if let Value::String(_found_val) = path_ref.clone().val() {
                        let no_value = Value::String("NO_VALUE".to_string());
                        let found_path = path_ref.clone().path();
                        let json_pointer = convert_to_json_pointer_path(&found_path);
                        let value_at_path = rdap_json_response
                            .pointer(&json_pointer)
                            .unwrap_or(&no_value);
                        if value_at_path.is_string() {
                            // grab the value at the end point of the JSON path
                            let end_of_path_value = match rdap_json_response.pointer(&json_pointer)
                            {
                                Some(value) => value.clone(),
                                None => {
                                    continue;
                                }
                            };
                            let replaced_json =
                                replace_with(rdap_json_response.clone(), &found_path, &mut |x| {
                                    // STRING ONLY! This is the only spot where we are ACTUALLY replacing or updating something
                                    if x.is_string() {
                                        match x.as_str() {
                                            Some("") => Some(json!("[[REDACTED]]")),
                                            _ => Some(json!("[[REDACTED]]")),
                                        }
                                    } else {
                                        Some(end_of_path_value.clone()) // it isn't a string, put it back in there
                                    }
                                });
                            match replaced_json {
                                Ok(new_json) => *rdap_json_response = new_json,
                                _ => {
                                    // why did we fail to modify the JSON?
                                }
                            };
                        }
                    }
                }
            }
            _ => {
                // do nothing
            }
        }
    }

    rdap_json_response
}

// utility functions
fn convert_to_json_pointer_path(path: &str) -> String {
    path.trim_start_matches('$')
        .replace('.', "/")
        .replace("['", "/")
        .replace("']", "")
        .replace('[', "/")
        .replace(']', "")
        .replace("//", "/")
}

fn get_string_from_map(map: &serde_json::Map<String, Value>, key: &str) -> String {
    map.get(key)
        .and_then(|v| v.as_str())
        .map(|s| s.to_string())
        .unwrap_or_default()
}

#[cfg(test)]
mod tests {
    use std::path::PathBuf;

    use goldenfile::Mint;
    use {
        serde_json::Value,
        std::{
            error::Error,
            fs::File,
            io::{Read, Write},
        },
    };

    static MINT_PATH: &str = "src/test_files/md/redacted";

    fn process_redacted_file(file_path: &PathBuf) -> Result<String, Box<dyn Error>> {
        let mut file = File::open(file_path)?;
        let mut contents = String::new();
        file.read_to_string(&mut contents)?;

        // this has to be setup very specifically, just like replace_redacted_items is setup.
        let mut rdap_json_response: Value = serde_json::from_str(&contents)?;
        let redacted_array_option = rdap_json_response["redacted"].as_array().cloned();
        // we are testing parse_redacted_json here -- just the JSON transforms
        if let Some(redacted_array) = redacted_array_option {
            crate::md::redacted::convert_redactions(&mut rdap_json_response, &redacted_array);
        } else {
            panic!("No redacted array found in the JSON");
        }
        let pretty_json = serde_json::to_string_pretty(&rdap_json_response)?;
        println!("{}", pretty_json);
        Ok(pretty_json)
    }

    #[test]
    fn test_process_empty_value() {
        // GIVEN
        let response_file = PathBuf::from(MINT_PATH).join("example-1_empty_value.json");

        // WHEN
        let actual = process_redacted_file(&response_file).unwrap();

        // THEN
        let mut mint = Mint::new(MINT_PATH);
        let mut expected = mint
            .new_goldenfile("example-1_empty_value-expected.json")
            .unwrap();
        expected.write_all(actual.as_bytes()).unwrap();
    }

    #[test]
    fn test_process_partial_value() {
        // GIVEN
        let response_file = PathBuf::from(MINT_PATH).join("example-2_partial_value.json");

        // WHEN
        let actual = process_redacted_file(&response_file).unwrap();

        // THEN
        let mut mint = Mint::new(MINT_PATH);
        let mut expected = mint
            .new_goldenfile("example-2_partial_value-expected.json")
            .unwrap();
        expected.write_all(actual.as_bytes()).unwrap();
    }

    #[test]
    fn test_process_dont_replace_number() {
        // GIVEN
        let fname = "example-3-dont_replace_redaction_of_a_number.json";
        let response_file = PathBuf::from(MINT_PATH).join(fname);

        // WHEN
        let actual = process_redacted_file(&response_file).unwrap();

        // THEN
        let mut mint = Mint::new(MINT_PATH);
        let mut expected = mint.new_goldenfile(fname).unwrap();
        expected.write_all(actual.as_bytes()).unwrap();
    }
}
