use std::collections::HashMap;
use std::collections::hash_map::Entry;
use std::num::NonZeroUsize;

use camino::{Utf8Path, Utf8PathBuf};
use canonical_path::CanonicalPathBuf;
use libpijul::change::{BaseHunk, Hunk};
use libpijul::changestore::ChangeStore;
use libpijul::pristine::sanakirja::{MutTxn, SanakirjaError};
use libpijul::working_copy::WorkingCopy;
use libpijul::{ArcTxn, ChannelRef, RecordBuilder, TxnTExt};
use patricia_tree::GenericPatriciaMap;

#[derive(Debug, thiserror::Error)]
#[error(transparent)]
pub enum CreatePathStatesError {
    #[error("Unable to canonicalize root path `{root_path}`: {io_error:#?}")]
    CanonicalRoot {
        root_path: Utf8PathBuf,
        io_error: std::io::Error,
    },
    #[error("Failed to iterate through workspace: {0}")]
    Iteration(std::io::Error),
    #[error("Failed to check if path is tracked: {0}")]
    IsTracked(SanakirjaError),
}

#[derive(Debug, thiserror::Error)]
pub enum PathStatesError<C: std::error::Error + 'static, W: std::error::Error> {
    Create(#[from] CreatePathStatesError),
    Sanakirja(#[from] SanakirjaError),
    Record(#[from] libpijul::record::RecordError<C, W, MutTxn<()>>),
}

#[derive(Clone, Copy, Debug)]
pub enum TrackedState {
    Added,
    Removed,
    Modified,
    Moved,
    ModifiedAndMoved,
}

impl TrackedState {
    pub fn join_hunk(
        state: Option<Self>,
        hunk: &Hunk<Option<libpijul::Hash>, libpijul::change::Local>,
    ) -> Option<Self> {
        match hunk {
            BaseHunk::FileMove { .. } => match state {
                Some(TrackedState::Modified) => Some(TrackedState::ModifiedAndMoved),
                None => Some(TrackedState::Moved),
                Some(_) => unreachable!("{hunk:#?}"),
            },
            BaseHunk::FileDel { .. } => match state {
                None => Some(TrackedState::Removed),
                Some(_existing_state) => unreachable!("{hunk:#?}"),
            },
            BaseHunk::FileAdd { .. } => match state {
                None => Some(TrackedState::Added),
                Some(_existing_state) => unreachable!("{hunk:#?}"),
            },
            BaseHunk::Edit { .. } | BaseHunk::Replacement { .. } => match state {
                Some(TrackedState::Modified) => Some(TrackedState::Modified),
                Some(TrackedState::Moved) => Some(TrackedState::ModifiedAndMoved),
                None => Some(TrackedState::Modified),
                Some(_) => unreachable!("{hunk:#?}"),
            },
            // TODO: FileUndel
            // TODO: conflicts
            _ => state,
        }
    }
}

#[derive(Clone, Copy, Debug)]
pub enum PathState {
    Untracked,
    Tracked(TrackedState),
}

pub struct PathStates {
    states: GenericPatriciaMap<String, PathState>,
}

impl PathStates {
    pub fn new<C>(
        root: &Utf8Path,
        transaction: &ArcTxn<MutTxn<()>>,
        channel: &ChannelRef<MutTxn<()>>,
        file_system: &libpijul::working_copy::FileSystem,
        change_store: &C,
    ) -> Result<Self, PathStatesError<C::Error, std::io::Error>>
    where
        C: ChangeStore + Clone + Send + 'static,
    {
        let mut path_states = Self::from_untracked_states(root, file_system, transaction)?;
        path_states.update_tracked_states("", transaction, channel, file_system, change_store)?;

        Ok(path_states)
    }

    fn from_untracked_states(
        root: &Utf8Path,
        file_system: &libpijul::working_copy::FileSystem,
        transaction: &ArcTxn<MutTxn<()>>,
    ) -> Result<Self, CreatePathStatesError> {
        let canonical_path = CanonicalPathBuf::canonicalize(root).map_err(|io_error| {
            CreatePathStatesError::CanonicalRoot {
                root_path: root.to_path_buf(),
                io_error,
            }
        })?;

        let file_system_iterator = file_system
            .iterate_prefix_rec(
                canonical_path.clone(),
                canonical_path,
                false,
                std::thread::available_parallelism()
                    .unwrap_or(NonZeroUsize::MIN)
                    .get(),
                // Follow all paths
                |_path, _is_directory| true,
            )
            .map_err(CreatePathStatesError::Iteration)?;

        let mut untracked_states = GenericPatriciaMap::new();
        let read_transaction = transaction.read();

        for entry in file_system_iterator {
            let (path, _is_directory) = match entry {
                Ok((path, is_directory)) => (path, is_directory),
                Err(error) => {
                    tracing::error!(message = "Error traversing file system", %error);
                    continue;
                }
            };

            let utf8_path = match Utf8PathBuf::from_path_buf(path) {
                Ok(utf8_path) => utf8_path,
                Err(path) => {
                    tracing::error!(message = "Unable to convert PathBuf to Utf8PathBuf", ?path);
                    continue;
                }
            };

            if !read_transaction
                .is_tracked(utf8_path.as_str())
                .map_err(CreatePathStatesError::IsTracked)?
            {
                untracked_states.insert(utf8_path.into_string(), PathState::Untracked);
            }
        }

        Ok(Self {
            states: untracked_states,
        })
    }

    fn update_tracked_states<C, W>(
        &mut self,
        prefix: &str,
        transaction: &ArcTxn<MutTxn<()>>,
        channel: &ChannelRef<MutTxn<()>>,
        working_copy: &W,
        change_store: &C,
    ) -> Result<(), PathStatesError<C::Error, W::Error>>
    where
        C: ChangeStore + Clone + Send + 'static,
        W: WorkingCopy + Clone + Send + Sync + 'static,
    {
        let mut unrecorded_changes = RecordBuilder::new();
        unrecorded_changes.record(
            transaction.clone(),
            libpijul::Algorithm::default(),
            false, // TODO: check and document
            &libpijul::DEFAULT_SEPARATOR,
            channel.clone(),
            working_copy,
            change_store,
            prefix,
            1, // TODO: figure out concurrency model
        )?;
        let unrecorded_state = unrecorded_changes.finish();

        let mut updated_states = HashMap::new();

        for hunk in unrecorded_state.actions {
            let globalized_hunk = hunk.globalize(&*transaction.read())?;

            let entry = updated_states.entry(globalized_hunk.path().to_string());
            let existing_tracked_state = match &entry {
                Entry::Occupied(occupied_entry) => match occupied_entry.get() {
                    PathState::Untracked => None,
                    PathState::Tracked(tracked_state) => Some(*tracked_state),
                },
                Entry::Vacant(_vacant_entry) => None,
            };

            if let Some(updated_state) =
                TrackedState::join_hunk(existing_tracked_state, &globalized_hunk)
            {
                entry.insert_entry(PathState::Tracked(updated_state));
            } else {
                tracing::info!(message = "Skipping unrecorded hunk", ?globalized_hunk);
            }
        }

        // Overwrite the previous path states that were affected, and clear any previous paths
        // that no longer have an active state
        let mut paths_to_remove = Vec::new();
        for (path, existing_state) in self.states.iter_prefix_mut(prefix) {
            match updated_states.remove(&path) {
                Some(updated_state) => {
                    // Update an existing state
                    *existing_state = updated_state;
                }
                None => {
                    // There is currently no state attached to this path, so remove it
                    paths_to_remove.push(path);
                }
            }
        }

        // Actually clear the paths that no longer have an active state (and aren't untracked)
        let read_transaction = transaction.read();
        for outdated_path in paths_to_remove {
            if read_transaction.is_tracked(&outdated_path)? {
                self.states.remove(outdated_path);
            } else {
                self.states.insert(outdated_path, PathState::Untracked);
            }
        }

        // Insert any new paths that remain
        for (path, updated_state) in updated_states {
            self.states.insert(path, updated_state);
        }

        Ok(())
    }

    pub fn get_path_state(&self, path: &Utf8Path) -> Option<PathState> {
        self.states.get(path.as_str()).copied()
    }

    pub fn iter_path_states(&self) -> impl Iterator<Item = (Utf8PathBuf, PathState)> {
        self.states
            .iter()
            .map(|(path, state)| (Utf8PathBuf::from(path), *state))
    }

    // TODO: handle transitions:
    // - Added -> unrecorded
    // - Modified -> unmodified
    pub fn update_path_state<C, W>(
        &mut self,
        path: Utf8PathBuf,
        transaction: &ArcTxn<MutTxn<()>>,
        channel: &ChannelRef<MutTxn<()>>,
        working_copy: &W,
        change_store: &C,
    ) -> Result<(), PathStatesError<C::Error, W::Error>>
    where
        C: ChangeStore + Clone + Send + 'static,
        W: WorkingCopy + Clone + Send + Sync + 'static,
    {
        if transaction.read().is_tracked(path.as_str())? {
            self.update_tracked_states(
                path.as_str(),
                transaction,
                channel,
                working_copy,
                change_store,
            )?;
        } else {
            self.states.insert(path.into_string(), PathState::Untracked);
        }

        Ok(())
    }
}