use std::collections::HashMap;
use std::env;

use super::GitlabElement;
use glob::glob;
use log::{error, info};
use temp_env::with_var;

pub struct ParserUtils {}

#[derive(Debug, PartialEq, Clone)]
pub struct ComponentInfo {
    pub host: String,
    pub project: String,
    pub component: String,
    pub version: String,
}

impl ParserUtils {
    fn adjust_to_char_boundary(s: &str, byte_index: usize) -> usize {
        if byte_index == s.len() {
            return s.len();
        }
        s.char_indices()
            .take_while(|(i, _)| *i <= byte_index)
            .last()
            .map_or(0, |(i, _)| i)
    }

    pub fn strip_quotes(value: &str) -> &str {
        value.trim_matches('\'').trim_matches('"')
    }

    pub fn extract_word(line: &str, char_index: usize) -> Option<&str> {
        if char_index >= line.len() {
            return None;
        }

        let adjusted_index = Self::adjust_to_char_boundary(line, char_index);

        let start = line[..adjusted_index]
            .rfind(|c: char| c.is_whitespace() || c == '"' || c == '\'' || c == '[')
            .map_or(0, |index| index + 1);

        let end = line[adjusted_index..]
            .find(|c: char| c.is_whitespace() || c == '"' || c == '\'' || c == ']')
            .map_or(line.len(), |index| index + adjusted_index);

        Some(&line[start..end])
    }

    pub fn word_before_cursor(
        line: &str,
        char_index: usize,
        predicate: fn(c: char) -> bool,
    ) -> &str {
        if char_index == 0 || char_index > line.len() {
            return "";
        }

        let adjusted_index = Self::adjust_to_char_boundary(line, char_index);

        let start = line[..adjusted_index]
            .rfind(predicate)
            .map_or(0, |index| index + 1);

        if start == adjusted_index {
            return "";
        }

        &line[start..adjusted_index]
    }

    pub fn word_after_cursor(
        line: &str,
        char_index: usize,
        predicate: fn(c: char) -> bool,
    ) -> &str {
        if char_index >= line.len() {
            return "";
        }

        let adjusted_start = Self::adjust_to_char_boundary(line, char_index);

        let end = line[adjusted_start..]
            .char_indices()
            .find(|&(_, c)| predicate(c))
            .map_or(line.len(), |(idx, _)| adjusted_start + idx);

        &line[adjusted_start..end]
    }

    pub fn remote_path_to_hash(uri: &str) -> String {
        crc64::crc64(0, uri.as_bytes()).to_string()
    }

    pub fn extract_variable(line: &str, char_index: usize) -> Option<&str> {
        if char_index >= line.len() {
            return None;
        }

        let adjusted_index = Self::adjust_to_char_boundary(line, char_index);

        let start = line[..adjusted_index]
            .rfind(['$', '{'])
            .map_or(0, |index| index + 1);

        let end = line[adjusted_index..]
            .find(|c: char| !c.is_alphabetic() && c != '_')
            .map_or(line.len(), |index| index + adjusted_index);

        Some(&line[start..end])
    }

    pub fn get_component_dest_dir(cache_path: &str, component_info: &ComponentInfo) -> String {
        let components_path = format!("{cache_path}components/");
        format!(
            "{}{}/{}",
            components_path, component_info.project, component_info.version
        )
    }

    pub fn find_path_at_cursor(line: &str, cursor_pos: usize) -> (String, String) {
        if cursor_pos >= line.len() {
            return (String::new(), String::new());
        }

        let adjusted_pos = Self::adjust_to_char_boundary(line, cursor_pos);
        let char_cursor = line
            .char_indices()
            .take_while(|(i, _)| *i < adjusted_pos)
            .count();

        let mut current_component = String::new();
        let mut previous_components = String::new();

        let mut found_current = false;

        for i in (0..char_cursor).rev() {
            let c = line.chars().nth(i).unwrap();
            let p = {
                if i == 0 {
                    'X'
                } else {
                    line.chars().nth(i - 1).unwrap()
                }
            };

            if p.is_whitespace() || c == '"' {
                break;
            }

            if c == '/' || c == '\\' {
                if found_current {
                    previous_components.insert(0, c); // insert at the beginning since we're moving backwards
                } else {
                    found_current = true;
                }
            } else if found_current {
                previous_components.insert(0, c); // insert at the beginning since we're moving backwards
            } else {
                current_component.insert(0, c); // insert at the beginning since we're moving backwards
            }
        }

        (current_component, previous_components)
    }

    // Its used because component can be in four different places
    // TODO: test this
    pub fn get_component(repo_dest: &str, component_name: &str) -> anyhow::Result<GitlabElement> {
        let paths = vec![
            format!("{}/templates/{}.yml", repo_dest, component_name),
            format!("{}/templates/{}.yaml", repo_dest, component_name),
            format!("{}/templates/{}/template.yml", repo_dest, component_name),
            format!("{}/templates/{}/template.yaml", repo_dest, component_name),
        ];

        for p in paths {
            match std::fs::read_to_string(&p) {
                Ok(content) => {
                    return Ok(GitlabElement {
                        key: format!("file://{p}"),
                        content: Some(content),
                        uri: format!("file://{p}"),
                        range: super::Range {
                            start: super::LSPPosition {
                                line: 0,
                                character: 0,
                            },
                            end: super::LSPPosition {
                                line: 0,
                                character: 0,
                            },
                        },
                    });
                }
                Err(err) => {
                    info!("could not find component; path: {p}, got err: {err}");
                }
            }
        }
        Err(anyhow::anyhow!("could not find component"))
    }

    pub fn extract_component_from_uri(
        uri: &str,
        git_remote_uris: &[String],
    ) -> anyhow::Result<ComponentInfo> {
        let mut component_parts = uri.split('/').collect::<Vec<&str>>();
        if component_parts.len() < 2 {
            return Err(anyhow::anyhow!(
                "invalid component uri structure; got: {uri}"
            ));
        }

        let mut host = component_parts.remove(0).to_string();
        // check if CI_SERVER_FQDN is being used. If so check if env variable is being set else set
        // to to git host
        if host.contains("$CI_SERVER_FQDN") {
            let ci_server_fqdn =
                env::var("CI_SERVER_FQDN").unwrap_or_else(|_| git_remote_uris[0].clone());

            host = host.replace("$CI_SERVER_FQDN", &ci_server_fqdn);
        }

        let Some(component) = component_parts.pop() else {
            return Err(anyhow::anyhow!(
                "could not get last element from component uri"
            ));
        };

        let component_identificator = component.split('@').collect::<Vec<&str>>();
        if component_identificator.len() != 2 {
            return Err(anyhow::anyhow!(
                "currently supported are only components with versions"
            ));
        }

        Ok(ComponentInfo {
            host: host.clone(),
            component: component_identificator[0].to_string(),
            project: component_parts.join("/"),
            version: component_identificator[1].to_string(),
        })
    }

    /// Extracts the content after the YAML document separator `---` in a component template.
    /// Returns None if there is no separator or no meaningful content after it.
    pub fn get_component_jobs_content(content: &str) -> Option<String> {
        let separator = "\n---\n";
        let idx = content.find(separator)?;
        let after = content[idx + separator.len()..].trim();

        if after.is_empty() {
            return None;
        }

        Some(after.to_string())
    }

    pub(crate) fn is_glob(uri: &str) -> bool {
        // TODO: kinda should work?
        uri.contains('*')
    }

    pub fn gitlab_style_glob(pattern: &str) -> Vec<std::path::PathBuf> {
        let mut results = Vec::new();
        let mut excludes = Vec::new();

        // Generate exclude list. Because when using **/* in gitlab
        // it means it should not match files in the current parent folder.. Only nested subfolders
        // So this way I can find files that I can exclude from actual glob pattern of **/* which
        // matches all
        if pattern.contains("**/*") {
            let new_pattern = pattern.replace("**/*", "*");

            let files = match glob(&new_pattern) {
                Ok(files) => files,
                Err(err) => {
                    error!("error matching files: {err}");
                    return vec![];
                }
            };

            excludes.extend(files.flatten());
        }

        // Replace gitlab custom pattern with valid one
        let mut pattern = pattern.to_string();
        if pattern.contains("**.") {
            pattern = pattern.replace("**", "**/*");
        }

        let files = match glob(&pattern) {
            Ok(files) => files,
            Err(err) => {
                error!("error matching files: {err}");
                return vec![];
            }
        };

        // Exclude current folder files if **/* was specified -> exlcudes were populated.
        results.extend(files.flatten().filter(|x| !excludes.contains(x)));

        results
    }

    // Expands $VAR and ${VAR} using the given variables map, falling back to
    // process environment variables. Unknown variables are left untouched.
    pub fn expand_variables(input: &str, variables: &HashMap<String, String>) -> String {
        let re = regex::Regex::new(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)")
            .expect("Invalid REGEX");

        re.replace_all(input, |caps: &regex::Captures| {
            let name = caps
                .get(1)
                .or_else(|| caps.get(2))
                .expect("one group always matches")
                .as_str();

            match variables.get(name) {
                Some(value) => value.clone(),
                None => env::var(name).unwrap_or_else(|_| caps[0].to_string()),
            }
        })
        .to_string()
    }
}

#[cfg(test)]
mod tests {
    use core::panic;

    use super::*;

    #[test]
    fn test_extract_component_from_uri() {
        let component_uri = "gitlab.com/some-project/sub-project/component@1.0.0";
        let want = ComponentInfo {
            component: "component".to_string(),
            version: "1.0.0".to_string(),
            project: "some-project/sub-project".to_string(),
            host: "gitlab.com".to_string(),
        };

        let got =
            match ParserUtils::extract_component_from_uri(component_uri, &["test".to_string()]) {
                Ok(c) => c,
                Err(err) => panic!("unable to extract; got: {err}"),
            };

        assert_eq!(got, want);
    }

    #[test]
    fn test_extract_component_from_uri_ci_server_fqdn_no_environment_variable() {
        let component_uri = "$CI_SERVER_FQDN/some-project/sub-project/component@1.0.0";
        let want = ComponentInfo {
            component: "component".to_string(),
            version: "1.0.0".to_string(),
            project: "some-project/sub-project".to_string(),
            host: "test-host-uri".to_string(),
        };

        let got = match ParserUtils::extract_component_from_uri(
            component_uri,
            &["test-host-uri".to_string()],
        ) {
            Ok(c) => c,
            Err(err) => panic!("unable to extract; got: {err}"),
        };

        assert_eq!(got, want);
    }

    #[test]
    fn test_extract_component_from_uri_ci_server_fqdn_with_environment_variable() {
        with_var("CI_SERVER_FQDN", Some("env-host-uri"), || {
            let component_uri = "$CI_SERVER_FQDN/some-project/sub-project/component@1.0.0";
            let want = ComponentInfo {
                component: "component".to_string(),
                version: "1.0.0".to_string(),
                project: "some-project/sub-project".to_string(),
                host: "env-host-uri".to_string(),
            };
            let got = match ParserUtils::extract_component_from_uri(
                component_uri,
                &["test-host-uri".to_string()],
            ) {
                Ok(c) => c,
                Err(err) => panic!("unable to extract; got: {err}"),
            };
            assert_eq!(got, want);
        });
    }

    #[test]
    fn test_find_path_at_cursor() {
        let line = "/test/please/here";
        let cursor = 14;
        let (path, parent) = ParserUtils::find_path_at_cursor(line, cursor);
        assert_eq!(path, "h");
        assert_eq!(parent, "/test/please");
    }

    #[test]
    fn test_find_path_at_cursor_quotes() {
        let line = r#""/test/please/here""#;
        let cursor = 15;
        let (path, parent) = ParserUtils::find_path_at_cursor(line, cursor);
        assert_eq!(path, "h");
        assert_eq!(parent, "/test/please");
    }

    #[test]
    fn test_extract_word_with_emojis() {
        let line = "  stage: ✅ Test 🕵 Quality 🚀 Deploy";
        let char_index = 34; // Inside the 🚀 emoji (bytes 31..35)
        let result = ParserUtils::extract_word(line, char_index);
        // This should work with the fix
        assert_eq!(result, Some("🚀"));
    }

    #[test]
    fn test_split_component_content_returns_jobs_after_separator() {
        let content = "spec:\n  inputs: {}\n---\n.my_job:\n  variables:\n    FOO: bar\n";

        let jobs = ParserUtils::get_component_jobs_content(content);
        assert!(jobs.is_some());
        let jobs = jobs.unwrap();
        assert!(
            jobs.contains(".my_job:"),
            "Expected .my_job in jobs content, got: {jobs}",
        );
        assert!(
            !jobs.contains("spec:"),
            "Jobs content should not contain spec"
        );
    }

    #[test]
    fn test_split_component_content_no_separator() {
        let content = "spec:\n  inputs: {}\n";

        let jobs = ParserUtils::get_component_jobs_content(content);
        assert!(jobs.is_none());
    }

    #[test]
    fn test_split_component_content_empty_after_separator() {
        let content = "spec:\n  inputs: {}\n---\n";

        let jobs = ParserUtils::get_component_jobs_content(content);
        assert!(jobs.is_none());
    }

    #[test]
    fn test_adjust_to_char_boundary() {
        let line = "  extends: .";
        let char_idx = 12;

        let adjusted = ParserUtils::adjust_to_char_boundary(line, char_idx);

        assert_eq!(adjusted, char_idx);
    }

    #[test]
    fn test_expand_variables_from_map() {
        let variables = HashMap::from([(
            "_GITLAB_TEMPLATES_REPO".to_string(),
            "project/gitlab_templates".to_string(),
        )]);

        assert_eq!(
            ParserUtils::expand_variables("$_GITLAB_TEMPLATES_REPO", &variables),
            "project/gitlab_templates"
        );
        assert_eq!(
            ParserUtils::expand_variables("${_GITLAB_TEMPLATES_REPO}", &variables),
            "project/gitlab_templates"
        );
    }

    #[test]
    fn test_expand_variables_from_environment() {
        with_var("EXPAND_TEST_REF", Some("main"), || {
            assert_eq!(
                ParserUtils::expand_variables("$EXPAND_TEST_REF", &HashMap::new()),
                "main"
            );
        });
    }

    #[test]
    fn test_expand_variables_map_wins_over_environment() {
        with_var("EXPAND_TEST_REF", Some("from-env"), || {
            let variables =
                HashMap::from([("EXPAND_TEST_REF".to_string(), "from-map".to_string())]);

            assert_eq!(
                ParserUtils::expand_variables("$EXPAND_TEST_REF", &variables),
                "from-map"
            );
        });
    }

    #[test]
    fn test_expand_variables_unknown_left_untouched() {
        assert_eq!(
            ParserUtils::expand_variables("$THIS_IS_NOT_DEFINED_ANYWHERE/x", &HashMap::new()),
            "$THIS_IS_NOT_DEFINED_ANYWHERE/x"
        );
    }
}
