use crate::command::bump::prerelease::increment_prerelease;
use crate::conventional::changelog::release::Release;
use crate::conventional::commit::Commit;
use crate::git::error::TagError;
use crate::git::oid::OidOf;

use crate::conventional::error::BumpError as ConvBumpError;
use crate::conventional::version::IncrementCommand;
use crate::conventional::version::PreCommand;
use crate::git::repository::Repository;
use crate::git::tag::{Tag, TagLookUpOptions};
use crate::hook::{Hook, HookVersion, Hooks};
use crate::settings::{HookType, MonoRepoPackage, Settings};
use crate::BumpError;
use crate::{CocoGitto, COMMITS_METADATA, SETTINGS};
use anyhow::Result;
use anyhow::{bail, ensure, Context};
use colored::Colorize;
use conventional_commit_parser::commit::CommitType;
use globset::Glob;
use itertools::Itertools;
use log::{error, info, warn};
use semver::{BuildMetadata, Prerelease};
use std::default::Default;
use std::fmt;
use std::fmt::Write;
use std::process::exit;

mod monorepo;
mod package;
mod prerelease;
mod standard;

#[derive(Default)]
pub struct BumpOptions<'a> {
    pub increment: IncrementCommand,
    pub pre_release: Option<PreCommand<'a>>,
    pub build: Option<&'a str>,
    pub hooks_config: Option<&'a str>,
    pub annotated: Option<String>,
    pub dry_run: bool,
    pub skip_ci: bool,
    pub skip_ci_override: Option<String>,
    pub skip_untracked: bool,
    pub disable_bump_commit: bool,
    pub include_packages: bool,
}

#[derive(Default)]
pub struct PackageBumpOptions<'a> {
    pub package_name: &'a str,
    pub package: &'a MonoRepoPackage,
    pub increment: IncrementCommand,
    pub pre_release: Option<PreCommand<'a>>,
    pub build: Option<&'a str>,
    pub hooks_config: Option<&'a str>,
    pub annotated: Option<String>,
    pub dry_run: bool,
    pub skip_ci: bool,
    pub skip_ci_override: Option<String>,
    pub skip_untracked: bool,
    pub disable_bump_commit: bool,
}

struct BumpResult {
    current: Tag,
    next: Tag,
    had_commits: bool,
}

impl BumpResult {
    fn no_change(&self) -> bool {
        self.current.version == self.next.version
    }
}

impl<'a> BumpOptions<'a> {
    fn get_new_version(
        &self,
        repository: &Repository,
        package: Option<&str>,
        allow_empty: bool,
        increment: Option<IncrementCommand>,
    ) -> Result<BumpResult> {
        let tag_opts = package.map(TagLookUpOptions::package).unwrap_or_default();
        let current = match repository.get_latest_tag(tag_opts) {
            Ok(tag) => tag,
            Err(TagError::NoTag) => Tag::default(),
            Err(other) => bail!(other),
        };
        let current_prerelease = repository
            .get_latest_tag(tag_opts.include_pre_release())
            .ok()
            .filter(|tag| *tag > current);

        let increment = increment.unwrap_or_else(|| self.increment.clone());
        let (mut next, had_commits) = match current.bump(increment, repository) {
            Ok(tag) => (tag, true),
            Err(ConvBumpError::NoCommitFound) if allow_empty => (current.strip_metadata(), false),
            Err(other) => bail!(other),
        };

        // if prerelease exists, ensure the new tag is not smaller
        if let Some(pre_release) = &current_prerelease {
            if next < *pre_release {
                next.version.major = pre_release.version.major;
                next.version.minor = pre_release.version.minor;
                next.version.patch = pre_release.version.patch;
            }
        }

        if current.version != next.version {
            match self.pre_release {
                Some(PreCommand::Exact(pre)) => {
                    next.version.pre = Prerelease::new(pre)?;
                }
                Some(PreCommand::Auto(pattern)) => {
                    let pre = increment_prerelease(&current_prerelease, &next, pattern)?;
                    next.version.pre = Prerelease::new(&pre)?;
                }
                None => {}
            }

            if let Some(build) = self.build {
                next.version.build = BuildMetadata::new(build)?;
            }
        }

        // ensure version doesn't decrease
        if next < current {
            bail!(
                "{}:\n\t{} version MUST be greater than current one: {}\n",
                "SemVer Error".red(),
                "cause:".red(),
                format!("{next} <= {current}").red(),
            );
        }

        Ok(BumpResult {
            current,
            next,
            had_commits,
        })
    }
}

impl<'a> PackageBumpOptions<'a> {
    fn common(&self) -> BumpOptions<'a> {
        BumpOptions {
            increment: self.increment.clone(),
            pre_release: self.pre_release.clone(),
            build: self.build,
            hooks_config: self.hooks_config,
            annotated: self.annotated.clone(),
            dry_run: self.dry_run,
            skip_ci: self.skip_ci,
            skip_ci_override: self.skip_ci_override.clone(),
            skip_untracked: self.skip_untracked,
            disable_bump_commit: self.disable_bump_commit,
            include_packages: false,
        }
    }
}

struct HookRunOptions<'a> {
    hook_type: HookType,
    current_tag: Option<&'a HookVersion>,
    next_version: Option<&'a HookVersion>,
    hook_profile: Option<&'a str>,
    package_name: Option<&'a str>,
    package: Option<&'a MonoRepoPackage>,
}

impl<'a> HookRunOptions<'a> {
    pub fn post_bump() -> Self {
        Self {
            hook_type: HookType::PostBump,
            current_tag: None,
            next_version: None,
            hook_profile: None,
            package_name: None,
            package: None,
        }
    }

    pub fn pre_bump() -> Self {
        Self {
            hook_type: HookType::PreBump,
            current_tag: None,
            next_version: None,
            hook_profile: None,
            package_name: None,
            package: None,
        }
    }

    pub fn current_tag<'b>(mut self, version: Option<&'b HookVersion>) -> Self
    where
        'b: 'a,
    {
        self.current_tag = version;
        self
    }

    pub fn next_version<'b>(mut self, version: &'b HookVersion) -> Self
    where
        'b: 'a,
    {
        self.next_version = Some(version);
        self
    }

    pub fn hook_profile<'b>(mut self, profile: Option<&'b str>) -> Self
    where
        'b: 'a,
    {
        self.hook_profile = profile;
        self
    }

    pub fn package<'b>(mut self, name: &'b str, package: &'b MonoRepoPackage) -> Self
    where
        'b: 'a,
    {
        self.package_name = Some(name);
        self.package = Some(package);
        self
    }
}

impl CocoGitto {
    fn get_bump_revspec(&mut self, current_tag: &Tag) -> String {
        if current_tag.is_zero() {
            "..".to_string()
        } else {
            format!("{current_tag}..")
        }
    }

    pub fn unwrap_or_stash_and_exit<T>(&mut self, tag: &Tag, result: Result<T>) -> T {
        match result {
            Ok(res) => res,
            Err(err) => {
                self.repository
                    .stash_failed_version(tag.clone())
                    .expect("stash");
                error!(
                    "{}",
                    BumpError {
                        cause: err.to_string(),
                        version: tag.to_string(),
                        stash_number: 0,
                    }
                );

                exit(1);
            }
        }
    }

    fn pre_bump_checks(&mut self, skip_untracked: bool) -> Result<()> {
        if *SETTINGS == Settings::default() {
            let part1 = "Warning: using".yellow();
            let part2 = "with the default configuration. \n".yellow();
            let part3 = "You may want to create a".yellow();
            let part4 = "file in your project root to configure bumps.\n".yellow();
            warn!(
                "{} 'cog bump' {}{} 'cog.toml' {}",
                part1, part2, part3, part4
            );
        }
        let statuses = self.repository.get_statuses()?;

        if skip_untracked || SETTINGS.skip_untracked {
            eprintln!("{}", self.repository.get_statuses()?);
        } else {
            ensure!(statuses.0.is_empty(), "{}", self.repository.get_statuses()?);
        }

        if !SETTINGS.branch_whitelist.is_empty() {
            if let Some(branch) = self.repository.get_branch_shorthand() {
                let whitelist = &SETTINGS.branch_whitelist;
                let is_match = whitelist.iter().any(|pattern| {
                    let glob = Glob::new(pattern)
                        .expect("invalid glob pattern")
                        .compile_matcher();
                    glob.is_match(&branch)
                });

                ensure!(
                    is_match,
                    "No patterns matched in {:?} for branch '{}', bump is not allowed",
                    whitelist,
                    branch
                )
            }
        };

        Ok(())
    }

    /// The target version is not created yet when generating the changelog.
    pub fn get_changelog_with_target_version(&self, pattern: &str, tag: Tag) -> Result<Release> {
        let commit_range = self.repository.revwalk(pattern)?;
        let mut release = Release::try_from(commit_range)?;
        release.version = OidOf::Tag(tag);
        Ok(release)
    }

    /// The target package version is not created yet when generating the changelog.
    pub fn get_package_changelog_with_target_version(
        &self,
        pattern: &str,
        tag: Tag,
        package: &str,
    ) -> Result<Release> {
        let commit_range = self
            .repository
            .get_commit_range_for_package(pattern, package)?;

        let mut release = Release::try_from(commit_range)?;
        release.version = OidOf::Tag(tag);
        Ok(release)
    }

    /// The target global monorepo version is not created yet when generating the changelog.
    pub fn get_monorepo_global_changelog_for_version(
        &self,
        pattern: &str,
        from: OidOf,
        tag: Tag,
    ) -> Result<Release> {
        let commit_range = self
            .repository
            .get_commit_range_for_monorepo_global(pattern)?;

        let release = match Release::try_from(commit_range) {
            Ok(mut release) => {
                release.version = OidOf::Tag(tag);
                release
            }
            Err(_) => Release {
                version: OidOf::Tag(tag),
                from,
                date: Default::default(),
                commits: vec![],
                previous: None,
            },
        };

        Ok(release)
    }

    fn run_hooks(&self, options: HookRunOptions) -> Result<()> {
        let settings = Settings::get(&self.repository)?;

        let hooks: Vec<Hook> = match (options.package, options.hook_profile) {
            (None, Some(profile)) => settings
                .get_profile_hooks(profile, options.hook_type)
                .iter()
                .map(|s| s.parse())
                .enumerate()
                .map(|(idx, result)| {
                    result.context(format!(
                        "Cannot parse bump profile {profile} hook at index {idx}"
                    ))
                })
                .try_collect()?,

            (Some(package), Some(profile)) => {
                let hooks = package.get_profile_hooks(profile, options.hook_type);

                hooks
                    .iter()
                    .map(|s| s.parse())
                    .enumerate()
                    .map(|(idx, result)| {
                        result.context(format!(
                            "Cannot parse bump profile {profile} hook at index {idx}"
                        ))
                    })
                    .try_collect()?
            }
            (Some(package), None) => package
                .get_hooks(options.hook_type)
                .iter()
                .map(|s| s.parse())
                .enumerate()
                .map(|(idx, result)| result.context(format!("Cannot parse hook at index {idx}")))
                .try_collect()?,
            (None, None) => settings
                .get_hooks(options.hook_type)
                .iter()
                .map(|s| s.parse())
                .enumerate()
                .map(|(idx, result)| result.context(format!("Cannot parse hook at index {idx}")))
                .try_collect()?,
        };

        if !hooks.is_empty() {
            let hook_type = match options.hook_type {
                HookType::PreBump => "pre-bump",
                HookType::PostBump => "post-bump",
            };

            match options.package_name {
                None => {
                    let msg = format!("[{hook_type}]").underline().white().bold();
                    info!("{msg}")
                }
                Some(package_name) => {
                    let msg = format!("[{hook_type}-{package_name}]")
                        .underline()
                        .white()
                        .bold();
                    info!("{msg}")
                }
            }
        }

        for mut hook in hooks {
            hook.insert_versions(options.current_tag, options.next_version)?;
            let command = hook.to_string();
            let command = if command.chars().count() > 78 {
                &command[0..command.len()]
            } else {
                &command
            };
            info!("[{command}]");
            let package_path = options.package.map(|p| p.path.as_path());
            hook.run(package_path).context(hook.to_string())?;
            println!();
        }

        Ok(())
    }
}

impl Release {
    fn pretty_print_bump_summary(&self) -> Result<(), fmt::Error> {
        let conventional_commits: Vec<&Commit> = self
            .commits
            .iter()
            .map(|ch_commit| &ch_commit.commit)
            .collect();

        // Commits which type are neither feat, fix nor breaking changes
        // won't affect the version number.
        let mut non_bump_commits: Vec<&CommitType> = conventional_commits
            .iter()
            .filter_map(|commit: &&Commit| {
                let commit_config = COMMITS_METADATA.get(&commit.conventional.commit_type);
                match commit_config {
                    Some(commit_config)
                        if commit_config.bump_minor() || commit_config.bump_patch() =>
                    {
                        None
                    }
                    _ if commit.conventional.is_breaking_change => None,
                    _ => Some(&commit.conventional.commit_type),
                }
            })
            .collect();

        non_bump_commits.sort();

        let non_bump_commits: Vec<(usize, &CommitType)> = non_bump_commits
            .into_iter()
            .dedup_by_with_count(|c1, c2| c1 == c2)
            .collect();

        if !non_bump_commits.is_empty() {
            let mut skip_message = "  Skipping irrelevant commits:\n".to_string();
            for (count, commit_type) in non_bump_commits {
                writeln!(skip_message, "    - {}: {}", commit_type.as_ref(), count)?;
            }

            info!("{}", skip_message);
        }

        let bump_commits =
            conventional_commits
                .iter()
                .filter(|commit| match &commit.conventional.commit_type {
                    CommitType::Feature | CommitType::BugFix => true,
                    _commit_type if commit.conventional.is_breaking_change => true,
                    _ => false,
                });

        for commit in bump_commits {
            match &commit.conventional.commit_type {
                _commit_type if commit.conventional.is_breaking_change => {
                    info!(
                        "\t Found {} commit {} with type: {}",
                        "BREAKING CHANGE".red(),
                        commit.shorthand().blue(),
                        commit.conventional.commit_type.as_ref().yellow()
                    )
                }
                CommitType::Feature => {
                    info!("\tFound feature commit {}", commit.shorthand().blue())
                }
                CommitType::BugFix => info!("\tFound bug fix commit {}", commit.shorthand().blue()),
                _ => (),
            }
        }

        Ok(())
    }
}
