use std::collections::HashMap;
use std::collections::hash_map::Entry;
use std::fs::File;

use camino::Utf8Path;
use libpijul::change::ChangeHeader;
use libpijul::changestore::ChangeStore;
use libpijul::pristine::SerializedHash;
use libpijul::pristine::sanakirja::{MutTxn, SanakirjaError};
use libpijul::{ArcTxn, ChannelRef, TxnTExt};

#[derive(Clone, Debug)]
pub enum AuthorSource {
    Local(pijul_identity::Complete),
    Remote(pijul_identity::Complete),
    Unknown(String),
}

#[derive(Debug, thiserror::Error)]
pub enum GetAuthorsError<C: std::error::Error + 'static> {
    #[error("Unable to get log of changes: {0}")]
    Log(SanakirjaError),
    #[error("Error while iterating log: {0}")]
    LogEntry(SanakirjaError),
    #[error("Failed to get change header for {hash:?}: {error}")]
    ChangeHeader { hash: SerializedHash, error: C },
}

pub struct Authors {
    authors: HashMap<String, AuthorSource>,
}

impl Authors {
    pub fn new<C>(
        dot_directory: &Utf8Path,
        transaction: &ArcTxn<MutTxn<()>>,
        channel: &ChannelRef<MutTxn<()>>,
        change_store: &C,
    ) -> Result<Self, GetAuthorsError<C::Error>>
    where
        C: ChangeStore + Clone + Send + 'static,
    {
        let mut authors = Self {
            authors: HashMap::new(),
        };

        // Get local identities
        authors.insert_local_identities();

        // Get identities stored in the repository
        authors.insert_repository_identities(dot_directory, transaction, channel, change_store)?;

        Ok(authors)
    }

    fn insert_local_identities(&mut self) {
        match pijul_identity::Complete::load_all() {
            Ok(local_identities) => {
                for local_identity in local_identities {
                    let public_key_signature = local_identity.public_key.key.clone();

                    match self.authors.entry(public_key_signature.clone()) {
                        Entry::Occupied(occupied_entry) => {
                            tracing::error!(
                                message = "Duplicate local identities found",
                                public_key_signature,
                                previous_author = ?occupied_entry.get(),
                            );
                        }
                        Entry::Vacant(vacant_entry) => {
                            vacant_entry.insert(AuthorSource::Local(local_identity));
                        }
                    }
                }
            }
            Err(error) => tracing::error!(message = "Unable to load local identities", ?error),
        }
    }

    fn insert_repository_identities<C>(
        &mut self,
        dot_directory: &Utf8Path,
        transaction: &ArcTxn<MutTxn<()>>,
        channel: &ChannelRef<MutTxn<()>>,
        change_store: &C,
    ) -> Result<(), GetAuthorsError<C::Error>>
    where
        C: ChangeStore + Clone + Send + 'static,
    {
        let repository_identities_path = dot_directory.join("identities");

        let read_transaction = transaction.read();
        let log_entries = read_transaction
            .log(&*channel.read(), 0)
            .map_err(GetAuthorsError::Log)?;

        for log_entry in log_entries {
            let (_index, (serialized_hash, _merkle)) =
                log_entry.map_err(GetAuthorsError::LogEntry)?;
            let change_header =
                change_store
                    .get_header(&serialized_hash.into())
                    .map_err(|error| GetAuthorsError::ChangeHeader {
                        hash: *serialized_hash,
                        error,
                    })?;

            for serialized_author in &change_header.authors {
                match serialized_author.0.get("key") {
                    Some(public_key_signature) => {
                        if let Entry::Vacant(vacant_entry) =
                            self.authors.entry(public_key_signature.to_owned())
                        {
                            // Try to read from `.pijul/identities/{public_key_signature}`
                            let identity_path =
                                repository_identities_path.join(public_key_signature);

                            let author_source = match File::open(&identity_path) {
                                Ok(identity_file) => match serde_json::from_reader(identity_file) {
                                    Ok(remote_identity) => AuthorSource::Remote(remote_identity),
                                    Err(error) => {
                                        tracing::error!(
                                            message = "Failed to deserialize identity file",
                                            ?identity_path,
                                            ?error
                                        );

                                        AuthorSource::Unknown(public_key_signature.to_owned())
                                    }
                                },
                                Err(error) => {
                                    tracing::error!(
                                        message = "Failed to open identity file",
                                        ?identity_path,
                                        ?error
                                    );

                                    AuthorSource::Unknown(public_key_signature.to_owned())
                                }
                            };

                            vacant_entry.insert(author_source);
                        }
                    }
                    None => {
                        tracing::warn!(message = "Author missing `key` field", ?serialized_author);
                    }
                }
            }
        }

        Ok(())
    }

    pub fn authors_for_change(&self, change_header: &ChangeHeader) -> Vec<&AuthorSource> {
        let mut authors = Vec::new();

        for serialized_author in &change_header.authors {
            match serialized_author.0.get("key") {
                Some(public_key_signature) => match self.authors.get(public_key_signature) {
                    Some(author) => {
                        authors.push(author);
                    }
                    None => tracing::error!(
                        message = "No matching author for key",
                        public_key_signature,
                        ?change_header
                    ),
                },
                None => {
                    tracing::warn!(message = "Author missing `key` field", ?serialized_author);
                }
            }
        }

        authors
    }
}