use std::collections::HashMap;

use camino::{Utf8Path, Utf8PathBuf};
use pijul_extension::FileSystemRepository;
use pijul_extension::path_state::PathState;

use crate::vscode_sys::reference::{
    SourceControlRef, SourceControlResourceGroupRef, TextEditorRef,
};
use crate::vscode_sys::{self, SourceControlResourceState};
use crate::vscode_sys::{TextEditor, Uri};

// TODO: remove public fields
pub struct OpenRepository {
    pub repository: FileSystemRepository,
    pub source_control: SourceControlRef,
    open_editors: HashMap<Utf8PathBuf, TextEditorRef>,
    unrecorded_changes: SourceControlResourceGroupRef,
    untracked_paths: SourceControlResourceGroupRef,
}

impl OpenRepository {
    #[tracing::instrument(skip_all)]
    pub fn new(
        env: &napi::Env,
        // TODO: pass SourceControl directly instead of URI?
        repository_uri: &Uri,
        // open_editors: HashMap<Utf8PathBuf, TextEditor>,
        // TODO: thiserror
    ) -> Result<Self, napi::Error> {
        tracing::debug!(message = "Opening repository", uri = ?repository_uri.to_string()?);
        let repository_path = Utf8PathBuf::from(repository_uri.get_fs_path()?);

        let source_control =
            vscode_sys::scm::create_source_control(env, "pijul", "Pijul", repository_uri)?;

        let repository = FileSystemRepository::new(&repository_path).map_err(|error| {
            napi::Error::from_reason(format!(
                "cannot open workspace at {repository_path}: {error}"
            ))
        })?;

        // TODO: l10n_embed
        let unrecorded_changes = source_control.create_resource_group("changes", "Changes")?;
        let untracked_paths = source_control.create_resource_group("untracked", "Untracked")?;

        let open_repository = Self {
            source_control: source_control.create_ref()?,
            repository,
            unrecorded_changes: unrecorded_changes.create_ref()?,
            untracked_paths: untracked_paths.create_ref()?,
            open_editors: HashMap::new(),
        };
        open_repository.update_resource_states(env, &repository_path)?;

        Ok(open_repository)
    }

    #[tracing::instrument(skip_all)]
    pub fn register_text_editor(
        &mut self,
        path: Utf8PathBuf,
        text_editor: TextEditor,
    ) -> Result<(), napi::Error> {
        if self.repository.get_open_file(&path).is_none() {
            let document = text_editor.get_document()?;
            let file_contents = document.get_text(None)?;

            // TODO: proper error handling
            self.repository
                .create_open_file(path.clone(), file_contents)
                .map_err(|error| {
                    napi::Error::from_reason(format!("Unable to create open file: {error}"))
                })?;

            let text_editor_reference = text_editor.create_ref()?;
            self.open_editors
                .try_insert(path, text_editor_reference)
                .map_err(|_error| napi::Error::from_reason("Text editor already exists"))?;
        }

        Ok(())
    }

    #[tracing::instrument(skip_all)]
    pub fn get_text_editor<'env>(
        &self,
        env: &'env napi::Env,
        path: &Utf8Path,
    ) -> Result<Option<TextEditor<'env>>, napi::Error> {
        self.open_editors
            .get(path)
            .map(|reference| reference.get_inner(env))
            .transpose()
    }

    #[tracing::instrument(skip_all)]
    pub fn update_resource_states(
        &self,
        env: &napi::Env,
        // TODO: ideally caller wouldn't have to provide repository path
        repository_path: &Utf8Path,
    ) -> Result<(), napi::Error> {
        let mut modified_resource_states = Vec::new();
        let mut untracked_resource_states = Vec::new();
        for (relative_path, path_state) in self.repository.iter_path_states() {
            let absolute_path = repository_path.join(relative_path);
            let resource_uri = Uri::file(env, absolute_path.as_str())?;

            let resource_state = SourceControlResourceState::new(env, &resource_uri)?;

            match path_state {
                PathState::Untracked => untracked_resource_states.push(resource_state),
                PathState::Tracked(_tracked_state) => modified_resource_states.push(resource_state),
            }
        }
        self.unrecorded_changes
            .get_inner(env)?
            .set_resource_states(modified_resource_states)?;
        self.untracked_paths
            .get_inner(env)?
            .set_resource_states(untracked_resource_states)?;

        Ok(())
    }
}