use jj_lib::{
    config::StackedConfig,
    git::{
        self, GitFetch, GitFetchError, GitFetchPrepareError, GitImportError,
        GitRemoteManagementError, RemoteCallbacks,
    },
    op_heads_store::OpHeadsStoreError,
    op_store::OpStoreError,
    repo::{ReadonlyRepo, Repo, RepoLoaderError, StoreFactories},
    settings::{GitSettings, UserSettings},
    str_util::StringPattern,
    workspace::{
        DefaultWorkspaceLoaderFactory, Workspace, WorkspaceInitError, WorkspaceLoadError,
        WorkspaceLoaderFactory, default_working_copy_factories,
    },
};
use std::{
    collections::HashMap,
    path::{Path, PathBuf},
    sync::Arc,
};
use thiserror::Error;

pub mod transaction;

#[derive(Error, Debug)]
#[non_exhaustive]
pub enum CodeStateError {
    #[error("Failed to clone repository at {0}: {1}")]
    CloneError(PathBuf, CodeCloneError),
    #[error("Repository already exists: {0}")]
    RepositoryExists(PathBuf),
}

#[derive(Error, Debug)]
#[non_exhaustive]
pub enum CodeCloneError {
    #[error("Failed to initialize workspace: {0}")]
    WorkspaceInit(#[from] WorkspaceInitError),
    #[error("Failed to add remote: {0}")]
    RemoteAddError(#[from] GitRemoteManagementError),
    #[error("Failed to store operation: {0}")]
    WorkspaceOperationStoreError(#[from] OpStoreError),
    #[error("Failed to store heads: {0}")]
    WorkspaceOperationHeadsStoreError(#[from] OpHeadsStoreError),
    #[error("Failed to load workspace: {0}")]
    WorkspaceLoadError(#[from] WorkspaceLoadError),
    #[error("Failed to load repository: {0}")]
    RepositoryLoadError(#[from] RepoLoaderError),
    #[error("Failed to fetch repository: {0}")]
    FetchPrepareError(#[from] GitFetchPrepareError),
    #[error("Failed to fetch repository: {0}")]
    FetchError(#[from] GitFetchError),
    #[error("Failed to import refs: {0}")]
    ImportReferencesError(#[from] GitImportError),
}

pub struct CodeState {
    repository_path: PathBuf,
    repositories: HashMap<String, PathBuf>,
    user_settings: UserSettings,
}

impl CodeState {
    pub fn new(repository_path: PathBuf) -> Self {
        Self {
            repository_path,
            repositories: HashMap::new(),
            user_settings: UserSettings::from_config(StackedConfig::with_defaults())
                .expect("Failed to load user settings"),
        }
    }

    pub fn get_repository_directory(&self) -> &Path {
        &self.repository_path
    }

    pub fn add_repository_from_path(
        &mut self,
        path: PathBuf,
    ) -> Result<(Workspace, Arc<ReadonlyRepo>), CodeStateError> {
        let name = path
            .file_name()
            .expect("Failed to get file name")
            .to_string_lossy()
            .to_string();

        let workspace =
            Workspace::init_internal_git(&self.user_settings, &path).map_err(|err| {
                CodeStateError::CloneError(path.clone(), CodeCloneError::WorkspaceInit(err))
            })?;

        self.repositories.insert(name, path);

        Ok(workspace)
    }

    /// Adds a repository from a URL.
    /// The repository will be cloned to the repository path
    pub fn add_repository_from_url(
        &mut self,
        name: &str,
        url: &str,
    ) -> Result<(Workspace, Arc<ReadonlyRepo>), CodeStateError> {
        let path = std::path::absolute(self.repository_path.join(name)).unwrap();
        if path.exists() {
            return Err(CodeStateError::RepositoryExists(path));
        }

        // Create the repository path if it doesn't exist and the path/.jj
        // directory if it doesn't exist
        std::fs::create_dir_all(&path).expect("Failed to create repository path");

        let (_workspace, repository) = Workspace::init_internal_git(&self.user_settings, &path)
            .map_err(|err| {
                CodeStateError::CloneError(path.clone(), CodeCloneError::WorkspaceInit(err))
            })?;

        let remote_name = "upstream";

        git::add_remote(repository.store(), remote_name, url).map_err(|err| {
            CodeStateError::CloneError(path.clone(), CodeCloneError::RemoteAddError(err))
        })?;

        let loader = DefaultWorkspaceLoaderFactory.create(&path).map_err(|err| {
            CodeStateError::CloneError(path.clone(), CodeCloneError::WorkspaceLoadError(err))
        })?;
        let workspace = loader
            .load(
                &self.user_settings,
                &StoreFactories::default(),
                &default_working_copy_factories(),
            )
            .map_err(|err| {
                CodeStateError::CloneError(path.clone(), CodeCloneError::WorkspaceLoadError(err))
            })?;

        let op = workspace
            .repo_loader()
            .load_operation(repository.op_id())
            .map_err(|err| {
                CodeStateError::CloneError(
                    path.clone(),
                    CodeCloneError::WorkspaceOperationStoreError(err),
                )
            })?;

        let repository = workspace.repo_loader().load_at(&op).map_err(|err| {
            CodeStateError::CloneError(path.clone(), CodeCloneError::RepositoryLoadError(err))
        })?;

        let mut transaction = repository.start_transaction();

        let mutable_repository = transaction.repo_mut();

        let git_settings = GitSettings {
            auto_local_bookmark: true,
            abandon_unreachable_commits: true,
            ..Default::default()
        };

        let mut git_fetch = GitFetch::new(mutable_repository, &git_settings).map_err(|err| {
            CodeStateError::CloneError(path.clone(), CodeCloneError::FetchPrepareError(err))
        })?;
        git_fetch
            .fetch(
                remote_name,
                &[StringPattern::everything()],
                RemoteCallbacks::default(),
                None,
            )
            .map_err(|err| {
                CodeStateError::CloneError(path.clone(), CodeCloneError::FetchError(err))
            })?;

        git_fetch
            .get_default_branch(remote_name, RemoteCallbacks::default())
            .map_err(|err| {
                CodeStateError::CloneError(path.clone(), CodeCloneError::FetchError(err))
            })?;

        git_fetch.import_refs().map_err(|err| {
            CodeStateError::CloneError(path.clone(), CodeCloneError::ImportReferencesError(err))
        })?;

        transaction.commit("Initial commit").map_err(|err| {
            CodeStateError::CloneError(
                path.clone(),
                CodeCloneError::WorkspaceOperationHeadsStoreError(err),
            )
        })?;

        Ok((workspace, repository))
    }

    pub fn remove_repository(&mut self, name: &str) {
        self.repositories.remove(name);
    }

    pub fn get_repository(&self, name: &str) -> Result<PathBuf, CodeStateError> {
        let path = self.repositories.get(name);

        match path {
            Some(path) => Ok(path.clone()),
            None => Err(CodeStateError::RepositoryExists(
                self.repository_path.join(name),
            )),
        }
    }

    pub fn load_repository(
        &self,
        path: &Path,
    ) -> Result<(Workspace, Arc<ReadonlyRepo>), CodeStateError> {
        let workspace = Workspace::load(
            &self.user_settings,
            path,
            &StoreFactories::default(),
            &default_working_copy_factories(),
        )
        .map_err(|err| {
            CodeStateError::CloneError(path.to_path_buf(), CodeCloneError::WorkspaceLoadError(err))
        })?;

        let repository = workspace.repo_loader().load_at_head().map_err(|err| {
            CodeStateError::CloneError(path.to_path_buf(), CodeCloneError::RepositoryLoadError(err))
        })?;

        Ok((workspace, repository))
    }

    pub fn discover_repositories(&mut self) {
        if !self.repository_path.exists() {
            std::fs::create_dir_all(&self.repository_path)
                .expect("Failed to create repository path");
        }

        // Look for repositories in the repository path
        // and add them to the repositories map
        let paths =
            std::fs::read_dir(&self.repository_path).expect("Failed to read repository path");
        for entry in paths {
            let entry = entry.expect("Failed to read entry");
            let path = entry.path();
            if path.is_dir() {
                let name = path
                    .file_name()
                    .expect("Failed to get file name")
                    .to_string_lossy()
                    .to_string();
                self.repositories.insert(name, path);
            }
        }
    }
}
