use std::sync::Arc;

use crate::{
    encryption::{
        indexer::{IndexerInit, IndexesForQuery, QueryOp},
        EncryptionError, IndexTerm, JsonIndexer, MatchIndexer, OpeIndexer, OreIndexer, Plaintext,
        ScopedCipher, UniqueIndexer,
    },
    zerokms::IndexKey,
};
use zerokms_protocol::cipherstash_config::column::{Index, IndexType};

#[derive(Debug)]
pub struct QueryBuilder<T, C> {
    source: T,
    scoped_cipher: Arc<ScopedCipher<C>>,
    descriptor: Option<String>,
}

impl<T, C> QueryBuilder<T, C> {
    pub fn new(source: T, scoped_cipher: Arc<ScopedCipher<C>>) -> Self {
        Self {
            source,
            scoped_cipher,
            descriptor: None,
        }
    }

    pub fn query_with<I>(self, index: I, op: QueryOp) -> Result<IndexTerm, EncryptionError>
    where
        for<'t> I: IndexesForQuery<T, C>,
    {
        index.query_index(self, op)
    }

    pub fn with_descriptor(mut self, descriptor: impl Into<String>) -> Self {
        self.descriptor = Some(descriptor.into());
        self
    }

    pub(crate) fn plaintext(&self) -> &T {
        &self.source
    }

    pub(crate) fn index_key(&self) -> &IndexKey {
        self.scoped_cipher.index_key()
    }
}

pub trait Queryable<C> {
    type Output;

    fn build_queryable(
        self,
        scoped_cipher: Arc<ScopedCipher<C>>,
        op: QueryOp,
    ) -> Result<IndexTerm, EncryptionError>;
}

impl<C> Queryable<C> for (Index, Plaintext) {
    type Output = Plaintext;

    fn build_queryable(
        self,
        scoped_cipher: Arc<ScopedCipher<C>>,
        op: QueryOp,
    ) -> Result<IndexTerm, EncryptionError> {
        (&self.0, self.1).build_queryable(scoped_cipher, op)
    }
}

impl<C> Queryable<C> for (&Index, Plaintext) {
    type Output = Plaintext;

    fn build_queryable(
        self,
        scoped_cipher: Arc<ScopedCipher<C>>,
        op: QueryOp,
    ) -> Result<IndexTerm, EncryptionError> {
        let (index, plaintext) = self;
        let builder = QueryBuilder::new(plaintext, scoped_cipher);

        match &index.index_type {
            unique @ IndexType::Unique { .. } => {
                let index = UniqueIndexer::try_init(unique)?;
                builder.query_with(index, op)
            }
            match_ @ IndexType::Match { .. } => {
                let index = MatchIndexer::try_init(match_)?;
                builder.query_with(index, op)
            }
            ore @ IndexType::Ore => {
                let index = OreIndexer::try_init(ore)?;
                builder.query_with(index, op)
            }
            ope @ IndexType::Ope => {
                let index = OpeIndexer::try_init(ope)?;
                builder.query_with(index, op)
            }
            json @ IndexType::SteVec { .. } => {
                let index = JsonIndexer::try_init(json)?;
                builder.query_with(index, op)
            }
        }
    }
}
