mod v1_10;
mod v1_11;
mod v1_12;
mod v1_9;

use std::path::{Path, PathBuf};

use anyhow::{bail, Context};
use meilisearch_types::versioning::create_version_file;
use v1_10::v1_9_to_v1_10;
use v1_12::{v1_11_to_v1_12, v1_12_to_v1_12_3};

use crate::upgrade::v1_11::v1_10_to_v1_11;

pub struct OfflineUpgrade {
    pub db_path: PathBuf,
    pub current_version: (u32, u32, u32),
    pub target_version: (u32, u32, u32),
}

impl OfflineUpgrade {
    pub fn upgrade(self) -> anyhow::Result<()> {
        // Adding a version?
        //
        // 1. Update the LAST_SUPPORTED_UPGRADE_FROM_VERSION and LAST_SUPPORTED_UPGRADE_TO_VERSION.
        // 2. Add new version to the upgrade list if necessary
        // 3. Use `no_upgrade` as index for versions that are compatible.

        if self.current_version == self.target_version {
            println!("Database is already at the target version. Exiting.");
            return Ok(());
        }

        if self.current_version > self.target_version {
            bail!(
                "Cannot downgrade from {}.{}.{} to {}.{}.{}. Downgrade not supported",
                self.current_version.0,
                self.current_version.1,
                self.current_version.2,
                self.target_version.0,
                self.target_version.1,
                self.target_version.2
            );
        }

        const FIRST_SUPPORTED_UPGRADE_FROM_VERSION: &str = "1.9.0";
        const LAST_SUPPORTED_UPGRADE_FROM_VERSION: &str = "1.12.7";
        const FIRST_SUPPORTED_UPGRADE_TO_VERSION: &str = "1.10.0";
        const LAST_SUPPORTED_UPGRADE_TO_VERSION: &str = "1.12.7";

        let upgrade_list = [
            (v1_9_to_v1_10 as fn(&Path, u32, u32, u32) -> Result<(), anyhow::Error>, 1, 10, 0),
            (v1_10_to_v1_11, 1, 11, 0),
            (v1_11_to_v1_12, 1, 12, 0),
            (v1_12_to_v1_12_3, 1, 12, 3),
        ];

        let no_upgrade: usize = upgrade_list.len();

        let (current_major, current_minor, current_patch) = self.current_version;

        let start_at = match (current_major, current_minor, current_patch) {
            (1, 9, _) => 0,
            (1, 10, _) => 1,
            (1, 11, _) => 2,
            (1, 12, 0..=2) => 3,
            (1, 12, 3..=7) => no_upgrade,
            _ => {
                bail!("Unsupported current version {current_major}.{current_minor}.{current_patch}. Can only upgrade from versions in range [{}-{}]",
                      FIRST_SUPPORTED_UPGRADE_FROM_VERSION,
                      LAST_SUPPORTED_UPGRADE_FROM_VERSION);
            }
        };

        let (target_major, target_minor, target_patch) = self.target_version;

        let ends_at = match (target_major, target_minor, target_patch) {
            (1, 10, _) => 0,
            (1, 11, _) => 1,
            (1, 12, 0..=2) => 2,
            (1, 12, 3..=7) => 3,
            _ => {
                bail!("Unsupported target version {target_major}.{target_minor}.{target_patch}. Can only upgrade to versions in range [{}-{}]",
                      FIRST_SUPPORTED_UPGRADE_TO_VERSION,
                      LAST_SUPPORTED_UPGRADE_TO_VERSION);
            }
        };

        println!("Starting the upgrade from {current_major}.{current_minor}.{current_patch} to {target_major}.{target_minor}.{target_patch}");

        if start_at == no_upgrade {
            println!("No upgrade operation to perform, writing VERSION file");
            create_version_file(&self.db_path, target_major, target_minor, target_patch)
                .context("while writing VERSION file after the upgrade")?;
            println!("Success");
            return Ok(());
        }

        #[allow(clippy::needless_range_loop)]
        for index in start_at..=ends_at {
            let (func, major, minor, patch) = upgrade_list[index];
            (func)(&self.db_path, current_major, current_minor, current_patch)?;
            println!("Done");
            // We're writing the version file just in case an issue arise _while_ upgrading.
            // We don't want the DB to fail in an unknown state.
            println!("Writing VERSION file");

            create_version_file(&self.db_path, major, minor, patch)
                .context("while writing VERSION file after the upgrade")?;
        }

        println!("Success");

        Ok(())
    }
}
