use std::{
    fs::File,
    io::{self, BufRead},
};

fn unescape_chars(value: &str) -> String {
    let mut output = String::new();
    let mut chars = value.chars();

    while let Some(char) = chars.next() {
        // If it's a normal character, continue
        if char != '\\' {
            output.push(char);
            continue;
        }

        // Check the next character
        match chars.next() {
            Some('n') => output.push('\n'),
            Some('t') => output.push('\t'),
            Some('r') => output.push('\r'),
            Some('"') => output.push('"'),
            Some('\'') => output.push('\''),
            Some('\\') => output.push('\\'),
            Some(other_value) => {
                // Unhandled case, push as is
                output.push('\\');
                output.push(other_value)
            }
            // end
            None => output.push('\\'),
        }
    }

    output
}

fn strip_unquoted_inline_comment(value: &str) -> &str {
    let mut previous_char_was_whitespace = false;

    for (idx, ch) in value.char_indices() {
        if ch == '#' && previous_char_was_whitespace {
            // We hit a comment
            if let Some(value) = value.get(..idx) {
                return value.trim_end();
            }
        }

        previous_char_was_whitespace = ch.is_whitespace();
    }

    value
}

fn find_closing_wrapper_idx(value: &str, wrapper: char, allow_escapes: bool) -> Option<usize> {
    if !value.starts_with(wrapper) {
        return None;
    }

    let mut escaped = false;

    for (idx, char) in value.char_indices().skip(1) {
        if allow_escapes && escaped {
            escaped = false;
            continue;
        }

        if allow_escapes && char == '\\' {
            escaped = true;
            continue;
        }

        if char == wrapper {
            return Some(idx);
        }
    }

    None
}

fn strip_wrapped_value(value: &str, wrapper: char, allow_escapes: bool) -> Option<&str> {
    let final_index = find_closing_wrapper_idx(value, wrapper, allow_escapes)?;

    value.get(1..final_index)
}

fn cleanup((key, value): (String, String)) -> (String, String) {
    let key = key.trim().to_string();
    let value = value.trim();

    if let Some(stripped_value) = strip_wrapped_value(value, '"', true) {
        // Double quotes wrapped
        return (key, unescape_chars(stripped_value));
    }

    if let Some(stripped_value) = strip_wrapped_value(value, '\'', false) {
        // Single quotes wrapped stay literal
        return (key, stripped_value.to_string());
    }

    // Non quoted value, remove any comments at the end
    return (key, strip_unquoted_inline_comment(value).to_string());
}

pub fn load() -> Result<(), std::io::Error> {
    load_from(".env")
}

pub fn load_from(file_path: impl AsRef<std::path::Path>) -> Result<(), std::io::Error> {
    for (key, value) in parse_env_contents(file_path)?.into_iter() {
        unsafe {
            std::env::set_var(key, value);
        }
    }

    Ok(())
}

fn parse_env_contents(
    file_path: impl AsRef<std::path::Path>,
) -> Result<Vec<(String, String)>, std::io::Error> {
    let file_path = file_path.as_ref();

    let file = File::open(file_path)?;

    let reader = io::BufReader::new(file);

    let mut is_active_multiline = false;
    let mut multiline_current_key = String::from("");
    let mut multiline_current_value = String::from("");

    let mut raw_keys_and_values = Vec::<(String, String)>::new();

    // Go through each line and parse each key / value into its own
    for (index, line) in reader.lines().enumerate() {
        match line {
            Ok(line) => {
                if is_active_multiline {
                    // Rest are multi line values that should be appended
                    // Do this check before splitting on '=' as a multiline comment wiht A=B would then be treated
                    // as its own value
                    multiline_current_value.push_str("\n");

                    let last_quote_idx = line.rfind(r#"""#);

                    if let Some(last_quote_idx) = last_quote_idx {
                        let quote_is_escaped = line
                            .as_bytes()
                            .get(last_quote_idx.saturating_sub(1))
                            .is_some_and(|byte| *byte == b'\\');

                        if !quote_is_escaped {
                            // If the 2nd to last quote is not escaped
                            let end_idx = last_quote_idx + 1;

                            if let Some(end_value) = line.get(..end_idx) {
                                multiline_current_value.push_str(end_value);
                            }

                            raw_keys_and_values.push((
                                multiline_current_key.clone(),
                                multiline_current_value.clone(),
                            ));

                            // Reset things for the next inputs
                            multiline_current_key.clear();
                            multiline_current_value.clear();
                            is_active_multiline = false;
                            continue;
                        }
                    }

                    // Add the current value
                    multiline_current_value.push_str(&line);
                    continue;
                }

                if let Some((key, value)) = line.split_once('=') {
                    let trimmed_line = line.trim();

                    if trimmed_line.is_empty() || trimmed_line.starts_with("#") {
                        // ignore comments
                        continue;
                    }
                    let trimmed_value = value.trim();

                    // Double quote wrapped lines are complete if their closing quote appears on this line
                    if trimmed_value.starts_with('"') {
                        if find_closing_wrapper_idx(trimmed_value, '"', true).is_some() {
                            is_active_multiline = false;
                            raw_keys_and_values.push((key.to_string(), value.to_string()));
                            continue;
                        }

                        // Start of a multi line value
                        is_active_multiline = true;
                        multiline_current_key = key.to_string();
                        // only trim the start of a multiline value
                        multiline_current_value = value.trim_start().to_string();
                        continue; // go to next line
                    }

                    // Single quote wrapped lines are complete if their closing quote appears on this line
                    if trimmed_value.starts_with('\'') {
                        if find_closing_wrapper_idx(trimmed_value, '\'', false).is_some() {
                            is_active_multiline = false;
                            raw_keys_and_values.push((key.to_string(), value.to_string()));
                            continue;
                        }
                    }

                    // Unquoted/unwrapped values are complete on one line
                    if !trimmed_value.starts_with(r#"""#) && !trimmed_value.starts_with(r#"'"#) {
                        is_active_multiline = false;
                        raw_keys_and_values.push((key.to_string(), value.to_string()));
                        continue;
                    }
                } else {
                    // println!("Line {} was skipped. Value: '{line}'", index + 1)
                }
            }
            Err(err) => {
                println!("An error occurred reading line {}. Error: {err}", index + 1);
                continue;
            }
        }
    }

    // Not able to terminate
    if is_active_multiline {
        println!(
            "Unable to parse value for key '{}'. Found starting quote but no closing quote.",
            multiline_current_key.trim()
        );
    }

    Ok(raw_keys_and_values
        .into_iter()
        // Clean everything up at the end
        .map(cleanup)
        .collect())
}

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

    fn parse_test_env(name: &str, contents: &str) -> Vec<(String, String)> {
        let path = std::env::temp_dir().join(format!("snv-{name}-{}.env", std::process::id()));
        std::fs::write(&path, contents).unwrap();

        let parsed = parse_env_contents(&path).unwrap();
        let _ = std::fs::remove_file(path);

        parsed
    }

    #[test]
    fn test_unescape_chars_newline() {
        let input = r#"hello\nworld"#;
        let expected_output = "hello\nworld";

        assert_eq!(unescape_chars(input), expected_output);
    }

    #[test]
    fn test_unescape_chars_tab() {
        let input = r#"hello\tworld"#;
        let expected_output = "hello\tworld";

        assert_eq!(unescape_chars(input), expected_output);
    }

    #[test]
    fn test_unescape_chars_return() {
        let input = r#"hello\rworld"#;
        let expected_output = "hello\rworld";

        assert_eq!(unescape_chars(input), expected_output);
    }

    #[test]
    fn test_unescape_chars_double_quote() {
        let input = r#"hello\"world"#;
        let expected_output = "hello\"world";

        assert_eq!(unescape_chars(input), expected_output);
    }

    #[test]
    fn test_unescape_chars_single_quote() {
        let input = r#"hello\'world"#;
        let expected_output = "hello'world";

        assert_eq!(unescape_chars(input), expected_output);
    }

    #[test]
    fn test_unescape_chars_backslash() {
        let input = r#"hello\\world"#;
        let expected_output = "hello\\world";

        assert_eq!(unescape_chars(input), expected_output);
    }

    #[test]
    fn test_unescape_chars_no_escape_chars() {
        let input = "hello world";
        let expected_output = "hello world";

        assert_eq!(unescape_chars(input), expected_output);
    }

    #[test]
    fn test_unescape_chars_trailing_backslash() {
        let input = r#"hello world\"#;
        let expected_output = r#"hello world\"#;

        assert_eq!(unescape_chars(input), expected_output);
    }

    #[test]
    fn test_unescape_chars_unknown_left_as_is() {
        let input = r#"hello\-world"#;
        let expected_output = r#"hello\-world"#;

        assert_eq!(unescape_chars(input), expected_output);
    }

    #[test]
    fn test_unescaped_chars_no_newline_on_backslash() {
        let input = r#"c:\\new-folder"#;
        let expected_output = r#"c:\new-folder"#;
        assert_eq!(unescape_chars(input), expected_output);
    }

    #[test]
    fn test_unescaped_chars_multiple_escapes() {
        let input = r#"hello\nworld\"jose here\""#;
        let expected_output = "hello\nworld\"jose here\"";
        assert_eq!(unescape_chars(input), expected_output);
    }

    #[test]
    fn test_cleanup_trims_keys() {
        let input = (" key ".into(), "value".into());
        assert_eq!(cleanup(input), ("key".into(), "value".into()));
    }

    #[test]
    fn test_cleanup_trims_values() {
        let input = ("key".into(), " value ".into());
        assert_eq!(cleanup(input), ("key".into(), "value".into()));
    }

    #[test]
    fn test_cleanup_parses_double_quotes() {
        let input = ("key".into(), r#""hello\nworld""#.into());
        assert_eq!(cleanup(input), ("key".into(), "hello\nworld".into()));
    }

    #[test]
    fn test_cleanup_parses_single_quotes() {
        let input = ("key".into(), r#"'beans'"#.into());
        assert_eq!(cleanup(input), ("key".into(), "beans".into()));
    }

    #[test]
    fn test_cleanup_single_quotes_stay_literal() {
        let input = ("key".into(), r#"'test\n\\n\r'"#.into());
        assert_eq!(cleanup(input), ("key".into(), r#"test\n\\n\r"#.into()));
    }

    #[test]
    fn test_parse_env_contents_return_empty_if_empty() {
        assert_eq!(parse_test_env("empty", ""), Vec::<(String, String)>::new());
    }

    #[test]
    fn test_parse_env_contents_skips_comment() {
        assert_eq!(
            parse_test_env("comment", "# how are you"),
            Vec::<(String, String)>::new()
        );
    }

    #[test]
    fn test_parse_env_contents_skips_no_delimiter() {
        assert_eq!(
            parse_test_env("no-delimiter", "API_KEYisEMPTY"),
            Vec::<(String, String)>::new()
        );
    }

    #[test]
    fn test_parse_env_contents_happy_path_double_quotes() {
        assert_eq!(
            parse_test_env("double-quotes", r#"API_KEY="beans""#),
            vec![("API_KEY".into(), "beans".into())]
        );
    }

    #[test]
    fn test_parse_env_contents_happy_path_single_quotes() {
        assert_eq!(
            parse_test_env("single-quotes", r#"API_KEY='beans'"#),
            vec![("API_KEY".into(), "beans".into())]
        );
    }

    #[test]
    fn test_parse_env_contents_happy_path_unquoted() {
        assert_eq!(
            parse_test_env("unquoted", r#"API_KEY=beans and guac"#),
            vec![("API_KEY".into(), "beans and guac".into())]
        );
    }

    #[test]
    fn test_parse_env_contents_inline_comments_double_quotes() {
        assert_eq!(
            parse_test_env(
                "double-quotes-inline-comment",
                r#"API_KEY="beans" # deprecated actually dont use this"#,
            ),
            vec![("API_KEY".into(), "beans".into())]
        );
    }

    #[test]
    fn test_parse_env_contents_inline_comments_within_double_quotes() {
        assert_eq!(
            parse_test_env(
                "double-quotes-comment-content",
                r#"API_KEY="beans #yeah " # deprecated actually dont use this"#,
            ),
            vec![("API_KEY".into(), "beans #yeah ".into())]
        );
    }

    #[test]
    fn test_parse_env_contents_inline_comments_single_quotes() {
        assert_eq!(
            parse_test_env(
                "single-quotes-inline-comment",
                r#"API_KEY='beans' # deprecated actually dont use this"#,
            ),
            vec![("API_KEY".into(), "beans".into())]
        );
    }

    #[test]
    fn test_parse_env_contents_with_whitespace_leading_comment() {
        assert_eq!(
            parse_test_env("leading-comment", r#"             #"#),
            Vec::<(String, String)>::new()
        );
    }

    #[test]
    fn test_parse_env_contents_comments_with_equals_are_ignored() {
        assert_eq!(
            parse_test_env("comment-with-equals", "# API_KEY=secret\nVISIBLE=value"),
            vec![("VISIBLE".into(), "value".into())]
        );
    }

    #[test]
    fn test_parse_env_contents_keeps_equals_after_split() {
        assert_eq!(
            parse_test_env(
                "equals-after-split",
                r#"DATABASE_URL=postgres://host/db?sslmode=require"#,
            ),
            vec![(
                "DATABASE_URL".into(),
                "postgres://host/db?sslmode=require".into()
            )]
        )
    }

    #[test]
    fn test_parse_env_contents_double_quotes_with_escaped_quotes() {
        assert_eq!(
            parse_test_env("escaped-double-quotes", r#"API_KEY="hello \"world\"""#),
            vec![("API_KEY".into(), "hello \"world\"".into())]
        );
    }

    #[test]
    fn test_parse_env_contents_double_quotes_keeps_newlines() {
        assert_eq!(
            parse_test_env("double-quotes-newlines", r#"PATH="c:\new-folder""#),
            vec![("PATH".into(), "c:\new-folder".into())]
        );
    }

    #[test]
    fn test_parse_env_contents_single_quote_exact() {
        let expected_output_value = r#"hello\n\""world""#;

        assert_eq!(
            parse_test_env("single-quote-exact", r#"API_KEY='hello\n\""world"'"#),
            vec![("API_KEY".into(), expected_output_value.into())]
        )
    }

    #[test]
    fn test_parse_env_contents_unquoted_with_comment_happy_path() {
        assert_eq!(
            parse_test_env(
                "unquoted-comment",
                r#"API_KEY=hello world # this is a comment"#
            ),
            vec![("API_KEY".into(), "hello world".into())]
        )
    }

    #[test]
    fn test_parse_env_contents_unquoted_with_no_comment_use_raw_text() {
        assert_eq!(
            parse_test_env("unquoted-hash-content", r#"DOCS_URL=example.com#section"#),
            vec![("DOCS_URL".into(), "example.com#section".into())]
        )
    }

    #[test]
    fn test_parse_env_contents_duplicate_keys_preserve_order() {
        assert_eq!(
            parse_test_env("duplicate-keys", "KEY=first\nKEY=second"),
            vec![
                ("KEY".into(), "first".into()),
                ("KEY".into(), "second".into())
            ]
        );
    }

    #[test]
    fn test_parse_env_contents_multi_line_parsing() {
        assert_eq!(
            parse_test_env("multiline", "PROMPT=\"HELLO\n\n WORLD\""),
            vec![("PROMPT".into(), "HELLO\n\n WORLD".into())]
        );
    }

    #[test]
    fn test_parse_env_contents_multi_line_with_equals_inside() {
        assert_eq!(
            parse_test_env("multiline-equals", "PROMPT=\"hello\nA=B\nworld\""),
            vec![("PROMPT".into(), "hello\nA=B\nworld".into())]
        );
    }

    #[test]
    fn test_parse_env_contents_multi_line_comments_are_content() {
        assert_eq!(
            parse_test_env("multiline-comments", "PROMPT=\"hello\nworld #how are ya\""),
            vec![("PROMPT".into(), "hello\nworld #how are ya".into())]
        );
    }

    #[test]
    fn test_parse_env_contents_multi_line_starts_after_escaped_quote() {
        assert_eq!(
            parse_test_env("multiline-escaped-quote", "PROMPT=\"hello \\\"\nworld\""),
            vec![("PROMPT".into(), "hello \"\nworld".into())]
        );
    }

    #[test]
    fn test_parse_env_contents_unterminated_multi_line_is_skipped() {
        assert_eq!(
            parse_test_env("unterminated-multiline", "PROMPT=\"hello\nworld"),
            Vec::<(String, String)>::new()
        );
    }
}
