use std::{ffi::OsStr, fs, path};

use keep_a_changelog::{
    changelog::ChangelogBuilder, ChangeKind, Changelog, ChangelogParseOptions, Release,
};
use url::Url;

use crate::Error;

#[derive(Debug)]
pub struct PrTitle {
    pub title: String,
    pub pr_id: Option<i64>,
    pub pr_url: Option<Url>,
    pub pr_body: Option<String>,
    pub commit_emoji: Option<String>,
    pub commit_type: Option<String>,
    pub commit_scope: Option<String>,
    pub commit_breaking: bool,
    pub section: Option<ChangeKind>,
    pub entry: String,
}

impl PrTitle {
    pub fn parse(title: &str) -> Result<Self, Error> {
        let re = regex::Regex::new(
            r"^(?P<emoji>.+\s)?(?P<type>[a-z]+)(?:\((?P<scope>.+)\))?(?P<breaking>!)?: (?P<description>.*)$$",
        )?;

        log::debug!("String to parse: `{title}`");

        let pr_title = if let Some(captures) = re.captures(title) {
            log::debug!("Captures: {captures:#?}");
            let commit_emoji = captures.name("emoji").map(|m| m.as_str().to_string());
            let commit_type = captures.name("type").map(|m| m.as_str().to_string());
            let commit_scope = captures.name("scope").map(|m| m.as_str().to_string());
            let commit_breaking = captures.name("breaking").is_some();
            let title = captures
                .name("description")
                .map(|m| m.as_str().to_string())
                .unwrap();

            Self {
                title,
                pr_id: None,
                pr_url: None,
                pr_body: None,
                commit_emoji,
                commit_type,
                commit_scope,
                commit_breaking,
                section: None,
                entry: String::new(),
            }
        } else {
            Self {
                title: title.to_string(),
                pr_id: None,
                pr_url: None,
                pr_body: None,
                commit_emoji: None,
                commit_type: None,
                commit_scope: None,
                commit_breaking: false,
                section: None,
                entry: String::new(),
            }
        };

        log::debug!("Parsed title: {pr_title:?}");

        Ok(pr_title)
    }

    pub fn set_pr_id(&mut self, id: i64) {
        self.pr_id = Some(id);
    }

    pub fn set_pr_url(&mut self, url: Url) {
        self.pr_url = Some(url);
    }

    pub fn set_pr_body(&mut self, body: Option<String>) {
        self.pr_body = body;
    }

    /// Returns true if a non-empty PR body is available, meaning the PR
    /// URL link should be included in the changelog entry.
    fn has_pr_body(&self) -> bool {
        match &self.pr_body {
            Some(body) => !body.is_empty(),
            None => true, // When body is not set, preserve existing behaviour (link present)
        }
    }

    pub fn calculate_section_and_entry(&mut self) {
        log::trace!("Calculating section and entry for `{self:#?}`");
        let mut section = ChangeKind::Changed;
        let mut entry = self.title.clone();

        log::debug!("Initial description `{entry}`");

        if let Some(commit_type) = &self.commit_type {
            match commit_type.as_str() {
                "feat" => section = ChangeKind::Added,
                "fix" => {
                    section = ChangeKind::Fixed;
                    if let Some(commit_scope) = &self.commit_scope {
                        log::trace!("Found scope `{commit_scope}`");
                        entry = format!("{}: {}", commit_scope, self.title);
                    }
                }
                _ => {
                    section = ChangeKind::Changed;
                    entry = format!("{}-{}", self.commit_type.as_ref().unwrap(), entry);

                    log::debug!("After checking for `feat` or `fix` type: `{entry}`");

                    if let Some(commit_scope) = &self.commit_scope {
                        log::trace!("Checking scope `{commit_scope}`");
                        match commit_scope.as_str() {
                            "security" => {
                                section = ChangeKind::Security;
                                entry = format!("Security: {}", self.title);
                            }
                            "deps" => {
                                section = ChangeKind::Security;
                                entry = format!("Dependencies: {}", self.title);
                            }
                            "remove" => {
                                section = ChangeKind::Removed;
                                entry = format!("Removed: {}", self.title);
                            }
                            "deprecate" => {
                                section = ChangeKind::Deprecated;
                                entry = format!("Deprecated: {}", self.title);
                            }
                            _ => {
                                section = ChangeKind::Changed;
                                let split_description = entry.splitn(2, '-').collect::<Vec<&str>>();
                                log::trace!("Split description: {split_description:#?}");
                                entry = format!(
                                    "{}({})-{}",
                                    split_description[0], commit_scope, split_description[1]
                                );
                            }
                        }
                    }
                }
            }
        }
        log::debug!("After checking scope `{entry}`");

        if self.commit_breaking {
            entry = format!("BREAKING: {entry}");
        }

        if let Some(id) = self.pr_id {
            if self.pr_url.is_some() && self.has_pr_body() {
                entry = format!("{entry}(pr [#{id}])");
            } else {
                entry = format!("{entry}(pr #{id})");
            }

            log::debug!("After checking pr id `{entry}`");
        };

        // Prepend the emoji to the entry
        if let Some(emoji) = &self.commit_emoji {
            entry = format!("{emoji}{entry}");
        }

        log::debug!("Final entry `{entry}`");
        self.section = Some(section);
        self.entry = entry;
    }

    fn section(&self) -> ChangeKind {
        match &self.section {
            Some(kind) => kind.clone(),
            None => ChangeKind::Changed,
        }
    }

    fn entry(&self) -> String {
        if self.entry.as_str() == "" {
            self.title.clone()
        } else {
            self.entry.clone()
        }
    }

    pub fn update_prlog(
        &mut self,
        log_file: &OsStr,
        opts: ChangelogParseOptions,
    ) -> Result<Option<(ChangeKind, String)>, Error> {
        let Some(log_file) = log_file.to_str() else {
            return Err(Error::InvalidPath(log_file.to_owned()));
        };

        let repo_url = match &self.pr_url {
            Some(pr_url) => {
                let url_string = pr_url.to_string();
                let components = url_string.split('/').collect::<Vec<&str>>();
                let url = format!("https://github.com/{}/{}", components[3], components[4]);
                Some(url)
            }
            None => None,
        };

        self.calculate_section_and_entry();

        log::trace!("Changelog entry:\n\n---\n{}\n---\n\n", self.entry());

        let mut change_log = if path::Path::new(log_file).exists() {
            let file_contents = fs::read_to_string(path::Path::new(log_file))?;
            log::trace!(
                "file contents:\n---\n{}\n---\n\n",
                file_contents
                    .lines()
                    .take(20)
                    .collect::<Vec<&str>>()
                    .join("\n")
            );
            if file_contents.contains(&self.entry) {
                log::trace!("The prlog exists and already contains the entry!");
                return Ok(None);
            } else {
                log::trace!("The prlog exists but does not contain the entry!");
            }

            Changelog::parse_from_file(log_file, Some(opts))
                .map_err(|e| Error::KeepAChangelog(e.to_string()))?
        } else {
            log::trace!("The prlog does not exist! Create a default prlog.");
            let mut prlog = ChangelogBuilder::default()
                .url(repo_url)
                .build()
                .map_err(|e| Error::KeepAChangelog(e.to_string()))?;
            log::debug!("Changelog: {prlog:#?}");
            let release = Release::builder()
                .build()
                .map_err(|e| Error::KeepAChangelog(e.to_string()))?;
            prlog.add_release(release);
            log::debug!("Changelog: {prlog:#?}");

            prlog
                .save_to_file(log_file)
                .map_err(|e| Error::KeepAChangelog(e.to_string()))?;
            prlog
        };

        // Get the unreleased section from the Changelog.
        // If there is no unreleased section create it and add it to the prlog
        let unreleased = if let Some(unreleased) = change_log.get_unreleased_mut() {
            unreleased
        } else {
            let release = Release::builder()
                .build()
                .map_err(|e| Error::KeepAChangelog(e.to_string()))?;
            change_log.add_release(release);
            let unreleased = change_log.get_unreleased_mut().unwrap();
            unreleased
        };

        match self.section() {
            ChangeKind::Added => {
                unreleased.added(self.entry());
            }
            ChangeKind::Fixed => {
                unreleased.fixed(self.entry());
            }
            ChangeKind::Security => {
                unreleased.security(self.entry());
            }
            ChangeKind::Removed => {
                unreleased.removed(self.entry());
            }
            ChangeKind::Deprecated => {
                unreleased.deprecated(self.entry());
            }
            ChangeKind::Changed => {
                unreleased.changed(self.entry());
            }
        }

        // add link to the url if it exists and the PR has a non-empty body
        if self.pr_url.is_some() && self.has_pr_body() {
            change_log.add_link(
                &format!("[#{}]:", self.pr_id.unwrap()),
                &self.pr_url.clone().unwrap().to_string(),
            );
        }

        change_log
            .save_to_file(log_file)
            .map_err(|e| Error::KeepAChangelog(e.to_string()))?;

        Ok(Some((self.section(), self.entry())))
    }
}

//test module
#[cfg(test)]
mod tests {
    use std::{
        fs::{self, File},
        io::Write,
        path::Path,
    };

    use log::LevelFilter;
    use rstest::rstest;
    use uuid::Uuid;

    use super::*;

    fn get_test_logger() {
        let mut builder = env_logger::Builder::new();
        builder.filter(None, LevelFilter::Debug);
        builder.format_timestamp_secs().format_module_path(false);
        let _ = builder.try_init();
    }

    #[test]
    fn test_pr_title_parse() {
        let pr_title = PrTitle::parse("feat: add new feature").unwrap();

        assert_eq!(pr_title.title, "add new feature");
        assert_eq!(pr_title.commit_type, Some("feat".to_string()));
        assert_eq!(pr_title.commit_scope, None);
        assert!(!pr_title.commit_breaking);

        let pr_title = PrTitle::parse("feat(core): add new feature").unwrap();
        assert_eq!(pr_title.title, "add new feature");
        assert_eq!(pr_title.commit_type, Some("feat".to_string()));
        assert_eq!(pr_title.commit_scope, Some("core".to_string()));
        assert!(!pr_title.commit_breaking);

        let pr_title = PrTitle::parse("feat(core)!: add new feature").unwrap();
        assert_eq!(pr_title.title, "add new feature");
        assert_eq!(pr_title.commit_type, Some("feat".to_string()));
        assert_eq!(pr_title.commit_scope, Some("core".to_string()));
        assert!(pr_title.commit_breaking);
    }

    #[test]
    fn test_pr_title_parse_with_breaking_scope() {
        let pr_title = PrTitle::parse("feat(core)!: add new feature").unwrap();
        assert_eq!(pr_title.title, "add new feature");
        assert_eq!(pr_title.commit_type, Some("feat".to_string()));
        assert_eq!(pr_title.commit_scope, Some("core".to_string()));
        assert!(pr_title.commit_breaking);
    }

    #[test]
    fn test_pr_title_parse_with_security_scope() {
        let pr_title = PrTitle::parse("fix(security): fix security vulnerability").unwrap();
        assert_eq!(pr_title.title, "fix security vulnerability");
        assert_eq!(pr_title.commit_type, Some("fix".to_string()));
        assert_eq!(pr_title.commit_scope, Some("security".to_string()));
        assert!(!pr_title.commit_breaking);
    }

    #[test]
    fn test_pr_title_parse_with_deprecate_scope() {
        let pr_title = PrTitle::parse("chore(deprecate): deprecate old feature").unwrap();
        assert_eq!(pr_title.title, "deprecate old feature");
        assert_eq!(pr_title.commit_type, Some("chore".to_string()));
        assert_eq!(pr_title.commit_scope, Some("deprecate".to_string()));
        assert!(!pr_title.commit_breaking);
    }

    #[test]
    fn test_pr_title_parse_without_scope() {
        let pr_title = PrTitle::parse("docs: update documentation").unwrap();
        assert_eq!(pr_title.title, "update documentation");
        assert_eq!(pr_title.commit_type, Some("docs".to_string()));
        assert_eq!(pr_title.commit_scope, None);
        assert!(!pr_title.commit_breaking);
    }

    #[test]
    fn test_pr_title_parse_issue_172() {
        let pr_title = PrTitle::parse(
            "chore(config.yml): update jerus-org/circleci-toolkit orb version to 0.4.0",
        )
        .unwrap();
        assert_eq!(
            pr_title.title,
            "update jerus-org/circleci-toolkit orb version to 0.4.0"
        );
        assert_eq!(pr_title.commit_type, Some("chore".to_string()));
        assert_eq!(pr_title.commit_scope, Some("config.yml".to_string()));
        assert!(!pr_title.commit_breaking);
    }

    #[rstest]
    #[case(
        "feat: add new feature",
        Some(5),
        Some("https://github.com/jerus-org/pcu/pull/5"),
        ChangeKind::Added,
        "add new feature(pr [#5])"
    )]
    #[case(
        "✨ feat: add new feature",
        Some(5),
        Some("https://github.com/jerus-org/pcu/pull/5"),
        ChangeKind::Added,
        "✨ add new feature(pr [#5])"
    )]
    #[case(
        "feat: add new feature",
        Some(5),
        None,
        ChangeKind::Added,
        "add new feature(pr #5)"
    )]
    #[case(
        "feat: add new feature",
        None,
        Some("https://github.com/jerus-org/pcu/pull/5"),
        ChangeKind::Added,
        "add new feature"
    )]
    #[case(
        "feat: add new feature",
        None,
        None,
        ChangeKind::Added,
        "add new feature"
    )]
    #[case(
        "✨ feat: add new feature",
        None,
        None,
        ChangeKind::Added,
        "✨ add new feature"
    )]
    #[case(
        "fix: fix an existing feature",
        None,
        None,
        ChangeKind::Fixed,
        "fix an existing feature"
    )]
    #[case(
        "🐛 fix: fix an existing feature",
        None,
        None,
        ChangeKind::Fixed,
        "🐛 fix an existing feature"
    )]
    #[case(
        "style: fix typo and lint issues",
        None,
        None,
        ChangeKind::Changed,
        "style-fix typo and lint issues"
    )]
    #[case(
        "💄 style: fix typo and lint issues",
        None,
        None,
        ChangeKind::Changed,
        "💄 style-fix typo and lint issues"
    )]
    #[case(
        "test: update tests",
        None,
        None,
        ChangeKind::Changed,
        "test-update tests"
    )]
    #[case(
        "fix(security): Fix security vulnerability",
        None,
        None,
        ChangeKind::Fixed,
        "security: Fix security vulnerability"
    )]
    #[case(
        "chore(deps): Update dependencies",
        None,
        None,
        ChangeKind::Security,
        "Dependencies: Update dependencies"
    )]
    #[case(
        "🔧 chore(deps): Update dependencies",
        None,
        None,
        ChangeKind::Security,
        "🔧 Dependencies: Update dependencies"
    )]
    #[case(
        "refactor(remove): Remove unused code",
        None,
        None,
        ChangeKind::Removed,
        "Removed: Remove unused code"
    )]
    #[case(
        "♻️ refactor(remove): Remove unused code",
        None,
        None,
        ChangeKind::Removed,
        "♻️ Removed: Remove unused code"
    )]
    #[case(
        "docs(deprecate): Deprecate old API",
        None,
        None,
        ChangeKind::Deprecated,
        "Deprecated: Deprecate old API"
    )]
    #[case(
        "📚 docs(deprecate): Deprecate old API",
        None,
        None,
        ChangeKind::Deprecated,
        "📚 Deprecated: Deprecate old API"
    )]
    #[case(
        "ci(other-scope): Update CI configuration",
        None,
        None,
        ChangeKind::Changed,
        "ci(other-scope)-Update CI configuration"
    )]
    #[case(
        "👷 ci(other-scope): Update CI configuration",
        None,
        None,
        ChangeKind::Changed,
        "👷 ci(other-scope)-Update CI configuration"
    )]
    #[case(
        "test!: Update test cases",
        None,
        None,
        ChangeKind::Changed,
        "BREAKING: test-Update test cases"
    )]
    #[case::issue_172(
        "chore(config.yml): update jerus-org/circleci-toolkit orb version to 0.4.0",
        Some(6),
        Some("https://github.com/jerus-org/pcu/pull/6"),
        ChangeKind::Changed,
        "chore(config.yml)-update jerus-org/circleci-toolkit orb version to 0.4.0(pr [#6])"
    )]
    #[case::with_emoji(
        "✨ feat(ci): add optional flag for push failure handling",
        Some(6),
        Some("https://github.com/jerus-org/pcu/pull/6"),
        ChangeKind::Added,
        "✨ add optional flag for push failure handling(pr [#6])"
    )]
    fn test_calculate_kind_and_description(
        #[case] title: &str,
        #[case] pr_id: Option<i64>,
        #[case] pr_url: Option<&str>,
        #[case] expected_kind: ChangeKind,
        #[case] expected_description: &str,
    ) -> Result<(), Error> {
        get_test_logger();

        let mut pr_title = PrTitle::parse(title).unwrap();
        if let Some(id) = pr_id {
            pr_title.set_pr_id(id);
        }
        if let Some(url) = pr_url {
            let url = Url::parse(url)?;
            pr_title.set_pr_url(url);
        }
        pr_title.calculate_section_and_entry();
        assert_eq!(expected_kind, pr_title.section());
        assert_eq!(expected_description, pr_title.entry);

        Ok(())
    }

    #[rstest]
    fn test_update_change_log_added() -> Result<(), Error> {
        get_test_logger();

        let initial_content = fs::read_to_string("tests/data/initial_prlog.md")?;
        let expected_content = fs::read_to_string("tests/data/expected_prlog.md")?;

        let temp_dir_string = format!("tests/tmp/test-{}", Uuid::new_v4());
        let temp_dir = Path::new(&temp_dir_string);
        fs::create_dir_all(temp_dir)?;

        let file_name = temp_dir.join("PRLOG.md");
        log::debug!("filename : {file_name:?}");

        let mut file = File::create(&file_name)?;
        file.write_all(initial_content.as_bytes())?;

        let mut pr_title = PrTitle {
            title: "add new feature".to_string(),
            pr_id: Some(5),
            pr_url: Some(Url::parse("https://github.com/jerus-org/pcu/pull/5")?),
            pr_body: None,
            commit_emoji: None,
            commit_type: Some("feat".to_string()),
            commit_scope: None,
            commit_breaking: false,
            section: Some(ChangeKind::Added),
            entry: "add new feature".to_string(),
        };

        let file_name = &file_name.into_os_string();

        let opts = ChangelogParseOptions::default();

        pr_title.update_prlog(file_name, opts)?;

        let actual_content = fs::read_to_string(file_name)?;

        assert_eq!(actual_content, expected_content);

        // tidy up the test environment
        std::fs::remove_dir_all(temp_dir)?;

        Ok(())
    }

    #[rstest]
    fn test_update_change_log_added_issue_172() -> Result<(), Error> {
        get_test_logger();

        let initial_content = fs::read_to_string("tests/data/initial_prlog.md")?;
        let expected_content = fs::read_to_string("tests/data/expected_prlog_issue_172.md")?;

        let temp_dir_string = format!("tests/tmp/test-{}", Uuid::new_v4());
        let temp_dir = Path::new(&temp_dir_string);
        fs::create_dir_all(temp_dir)?;

        let file_name = temp_dir.join("PRLOG.md");
        log::debug!("filename : {file_name:?}");

        let mut file = File::create(&file_name)?;
        file.write_all(initial_content.as_bytes())?;

        let mut pr_title = PrTitle {
            title: "add new feature".to_string(),
            pr_id: Some(5),
            pr_url: Some(Url::parse("https://github.com/jerus-org/pcu/pull/5")?),
            pr_body: None,
            commit_emoji: None,
            commit_type: Some("feat".to_string()),
            commit_scope: None,
            commit_breaking: false,
            section: Some(ChangeKind::Added),
            entry: "add new feature".to_string(),
        };

        let file_name = &file_name.into_os_string();
        let opts = ChangelogParseOptions::default();

        pr_title.update_prlog(file_name, opts)?;

        let mut pr_title = PrTitle::parse(
            "chore(config.yml): update jerus-org/circleci-toolkit orb version to 0.4.0",
        )?;
        pr_title.set_pr_id(6);
        pr_title.set_pr_url(Url::parse("https://github.com/jerus-org/pcu/pull/6")?);
        pr_title.calculate_section_and_entry();

        let file_name = &file_name.to_os_string();
        let opts = ChangelogParseOptions::default();

        pr_title.update_prlog(file_name, opts)?;

        let actual_content = fs::read_to_string(file_name)?;

        assert_eq!(actual_content, expected_content);

        // tidy up the test environment
        std::fs::remove_dir_all(temp_dir)?;

        Ok(())
    }

    /// RED: issue #144 — PrTitle must have a `pr_body` field
    #[test]
    fn test_pr_title_has_pr_body_field() {
        let mut pr_title = PrTitle::parse("feat: add new feature").unwrap();
        pr_title.set_pr_body(Some("Some description".to_string()));
        assert_eq!(pr_title.pr_body, Some("Some description".to_string()));
    }

    /// RED: issue #144 — when pr_url is set but pr_body is empty,
    /// the entry must use `(pr #N)` not `(pr [#N])`
    #[test]
    fn test_empty_pr_body_produces_unlinked_entry() -> Result<(), Error> {
        let mut pr_title = PrTitle::parse("feat: add new feature").unwrap();
        pr_title.set_pr_id(5);
        pr_title.set_pr_url(Url::parse("https://github.com/jerus-org/pcu/pull/5")?);
        pr_title.set_pr_body(Some(String::new())); // empty body
        pr_title.calculate_section_and_entry();
        // With an empty body, the link should be suppressed → `(pr #5)`
        assert_eq!(pr_title.entry, "add new feature(pr #5)");
        Ok(())
    }

    /// RED: issue #144 — when pr_url is set and pr_body is non-empty,
    /// the entry must still use `(pr [#N])` (existing behaviour preserved)
    #[test]
    fn test_non_empty_pr_body_produces_linked_entry() -> Result<(), Error> {
        let mut pr_title = PrTitle::parse("feat: add new feature").unwrap();
        pr_title.set_pr_id(5);
        pr_title.set_pr_url(Url::parse("https://github.com/jerus-org/pcu/pull/5")?);
        pr_title.set_pr_body(Some("Some description".to_string()));
        pr_title.calculate_section_and_entry();
        // With a non-empty body, the link should be present → `(pr [#5])`
        assert_eq!(pr_title.entry, "add new feature(pr [#5])");
        Ok(())
    }

    /// RED: issue #144 — when pr_body is None (unset), existing behaviour
    /// should be preserved: pr_url present → use linked format
    #[test]
    fn test_no_pr_body_set_preserves_linked_entry() -> Result<(), Error> {
        let mut pr_title = PrTitle::parse("feat: add new feature").unwrap();
        pr_title.set_pr_id(5);
        pr_title.set_pr_url(Url::parse("https://github.com/jerus-org/pcu/pull/5")?);
        // pr_body left as None (not set)
        pr_title.calculate_section_and_entry();
        // Without a body set, the existing behaviour applies: link present
        assert_eq!(pr_title.entry, "add new feature(pr [#5])");
        Ok(())
    }
}
