use anyhow::{Context, Result};
use bitcoin_hashes::Hash;

use crate::chaindef::{OutPointHash, ScriptHash};
use crate::query::queryfilter::QueryFilter;
use crate::store::{db_decode, db_encode, Row};

use super::DBRow;

const SCRIPTHASHINDEX_CF: &str = "scripthash";

/**
 * A flag (1 byte) to define type of output to allow efficent
 * filtering.
 */
#[derive(Eq, PartialEq, Clone, Copy, Debug)]
pub enum OutputFlags {
    /// Funding output without tokens
    FundingNone = 1 << 1,
    /// Funding output with tokens
    FundingHasTokens = 1 << 2,
    /// Spending entry for output without tokens
    SpentNone = 1 << 3,
    /// Spending entry for output with tokens
    SpentHasTokens = 1 << 4,
}

#[derive(serde::Serialize, serde::Deserialize, PartialEq, Eq, Debug)]
pub enum ScriptHashIndexValue {
    /// Funding entry: txid and vout
    Funding { txid: [u8; 32], vout: u32 },
    /// Spending entry: full spend metadata
    Spending { txid: [u8; 32], vin: u32 },
}

/// Byte offset of the flags field in the encoded key.
/// scripthash (32) + height_be (4) = 36.
const FLAGS_BYTE_OFFSET: usize = 36;

#[derive(serde::Serialize, serde::Deserialize, PartialEq, Eq, Debug)]
pub struct ScriptHashIndexKey {
    pub scripthash: [u8; 32],
    /// Block height encoded as big-endian fixed-width bytes. Big-endian is
    /// required so that RocksDB's lexicographic byte comparison yields correct
    /// numeric ordering, enabling rscan to return the most recent (highest
    /// height) entries first. Fixed-width (not varint) is required because
    /// postcard uses variable-length int encoding, which would break
    /// byte-order comparisons.
    height_be: [u8; 4],
    flags: u8,
    // Outpointhash needs to be part of key to make it unique
    outpointhash: [u8; 32],
}

#[derive(serde::Serialize, serde::Deserialize, PartialEq, Eq, Debug)]
pub struct ScriptHashIndexRow {
    key: ScriptHashIndexKey,
    value: ScriptHashIndexValue,
}

impl ScriptHashIndexRow {
    pub fn new_funding(
        scripthash: &ScriptHash,
        outpointhash: &OutPointHash,
        flags: OutputFlags,
        txid: [u8; 32],
        vout: u32,
        height: u32,
    ) -> ScriptHashIndexRow {
        debug_assert!(matches!(
            flags,
            OutputFlags::FundingNone | OutputFlags::FundingHasTokens
        ));
        ScriptHashIndexRow {
            key: ScriptHashIndexKey {
                scripthash: scripthash.into_inner(),
                height_be: height.to_be_bytes(),
                flags: flags as u8,
                outpointhash: outpointhash.into_inner(),
            },
            value: ScriptHashIndexValue::Funding { txid, vout },
        }
    }

    pub fn new_spending(
        scripthash: &ScriptHash,
        outpointhash: &OutPointHash,
        flags: OutputFlags,
        txid: [u8; 32],
        vin: u32,
        height: u32,
    ) -> ScriptHashIndexRow {
        debug_assert!(matches!(
            flags,
            OutputFlags::SpentNone | OutputFlags::SpentHasTokens
        ));
        ScriptHashIndexRow {
            key: ScriptHashIndexKey {
                scripthash: scripthash.into_inner(),
                height_be: height.to_be_bytes(),
                flags: flags as u8,
                outpointhash: outpointhash.into_inner(),
            },
            value: ScriptHashIndexValue::Spending { txid, vin },
        }
    }

    pub fn filter_include_all(scripthash: [u8; 32]) -> Vec<u8> {
        scripthash.to_vec()
    }

    pub fn filter_by_outputs_and_inputs(
        scripthash: [u8; 32],
        filter: &QueryFilter,
    ) -> (Vec<u8>, Option<(u8, usize)>) {
        match (filter.token_only, filter.exclude_tokens) {
            (true, false) => (
                Self::filter_include_all(scripthash),
                Some((
                    OutputFlags::FundingHasTokens as u8 | OutputFlags::SpentHasTokens as u8,
                    FLAGS_BYTE_OFFSET,
                )),
            ),
            (false, true) => (
                Self::filter_include_all(scripthash),
                Some((
                    OutputFlags::FundingNone as u8 | OutputFlags::SpentNone as u8,
                    FLAGS_BYTE_OFFSET,
                )),
            ),
            (true, true) => (
                Self::filter_include_all(scripthash),
                Some((0, FLAGS_BYTE_OFFSET)),
            ),
            (false, false) => (Self::filter_include_all(scripthash), None),
        }
    }

    pub fn filter_by_outputs(
        scripthash: [u8; 32],
        filter: &QueryFilter,
    ) -> (Vec<u8>, Option<(u8, usize)>) {
        match (filter.token_only, filter.exclude_tokens) {
            (true, false) => (
                Self::filter_include_all(scripthash),
                Some((OutputFlags::FundingHasTokens as u8, FLAGS_BYTE_OFFSET)),
            ),
            (false, true) => (
                Self::filter_include_all(scripthash),
                Some((OutputFlags::FundingNone as u8, FLAGS_BYTE_OFFSET)),
            ),
            (true, true) => (
                Self::filter_include_all(scripthash),
                Some((0, FLAGS_BYTE_OFFSET)),
            ),
            (false, false) => (
                Self::filter_include_all(scripthash),
                Some((
                    OutputFlags::FundingNone as u8 | OutputFlags::FundingHasTokens as u8,
                    FLAGS_BYTE_OFFSET,
                )),
            ),
        }
    }

    pub fn filter_by_spends(
        scripthash: [u8; 32],
        filter: &QueryFilter,
    ) -> (Vec<u8>, Option<(u8, usize)>) {
        match (filter.token_only, filter.exclude_tokens) {
            (true, false) => (
                Self::filter_include_all(scripthash),
                Some((OutputFlags::SpentHasTokens as u8, FLAGS_BYTE_OFFSET)),
            ),
            (false, true) => (
                Self::filter_include_all(scripthash),
                Some((OutputFlags::SpentNone as u8, FLAGS_BYTE_OFFSET)),
            ),
            (true, true) => (
                Self::filter_include_all(scripthash),
                Some((0, FLAGS_BYTE_OFFSET)),
            ),
            (false, false) => (
                Self::filter_include_all(scripthash),
                Some((
                    OutputFlags::SpentNone as u8 | OutputFlags::SpentHasTokens as u8,
                    FLAGS_BYTE_OFFSET,
                )),
            ),
        }
    }

    pub fn outpointhash(&self) -> OutPointHash {
        OutPointHash::from_inner(self.key.outpointhash)
    }

    /// Get a reference to the inner outpointhash bytes (for efficient XOR hashing)
    pub fn outpointhash_inner(&self) -> &[u8; 32] {
        &self.key.outpointhash
    }

    pub fn has_token(&self) -> bool {
        matches!(self.key.flags as u32, x if x == OutputFlags::FundingHasTokens as u32 || x == OutputFlags::SpentHasTokens as u32)
    }

    pub fn get_height(&self) -> u32 {
        u32::from_be_bytes(self.key.height_be)
    }

    pub fn get_scripthash_inner(&self) -> &[u8; 32] {
        &self.key.scripthash
    }

    pub fn get_txid(&self) -> [u8; 32] {
        match &self.value {
            ScriptHashIndexValue::Funding { txid, .. } => *txid,
            ScriptHashIndexValue::Spending { txid, .. } => *txid,
        }
    }

    pub fn is_funding(&self) -> bool {
        matches!(self.key.flags as u32, x if x == OutputFlags::FundingNone as u32 || x == OutputFlags::FundingHasTokens as u32)
    }

    pub fn is_spending(&self) -> bool {
        matches!(self.key.flags as u32, x if x == OutputFlags::SpentNone as u32 || x == OutputFlags::SpentHasTokens as u32)
    }

    pub fn spending_txid(&self) -> Option<[u8; 32]> {
        match &self.value {
            ScriptHashIndexValue::Spending { txid, .. } => Some(*txid),
            _ => None,
        }
    }

    pub fn spending_vin(&self) -> Option<u32> {
        match &self.value {
            ScriptHashIndexValue::Spending { vin, .. } => Some(*vin),
            _ => None,
        }
    }
}

fn deserialize_value(value: &[u8]) -> Result<ScriptHashIndexValue> {
    db_decode(value).context("failed to deserialize ScriptHashIndexRow value")
}

impl DBRow for ScriptHashIndexRow {
    const CF: &'static str = SCRIPTHASHINDEX_CF;

    fn to_row(&self) -> Row {
        Row {
            key: db_encode(&self.key).unwrap().into_boxed_slice(),
            value: db_encode(&self.value).unwrap().into_boxed_slice(),
        }
    }

    fn from_row(row: &Row) -> Self {
        let value = deserialize_value(&row.value).expect("failed to parse ScriptHashIndexRow");
        Self {
            key: db_decode(&row.key).expect("failed to read key for ScriptHashIndexRow"),
            value,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn test_to_from_row() {
        let txid = [1u8; 32];
        let row = ScriptHashIndexRow::new_funding(
            &ScriptHash::hash(&[42]),
            &OutPointHash::hash(&[11]),
            OutputFlags::FundingHasTokens,
            txid,
            5,
            42,
        );
        let decoded_row = ScriptHashIndexRow::from_row(&row.to_row());
        assert_eq!(row, decoded_row);
        assert_eq!(row.get_txid(), txid);
        assert_eq!(row.get_height(), 42);
    }

    #[test]
    fn test_spending_row() {
        let txid = [1u8; 32];
        let row = ScriptHashIndexRow::new_spending(
            &ScriptHash::hash(&[42]),
            &OutPointHash::hash(&[11]),
            OutputFlags::SpentHasTokens,
            txid,
            5,
            100,
        );
        assert!(row.is_spending());
        assert!(row.has_token());
        assert_eq!(row.get_height(), 100);
        assert_eq!(row.spending_txid(), Some(txid));
        assert_eq!(row.spending_vin(), Some(5));

        let decoded_row = ScriptHashIndexRow::from_row(&row.to_row());
        assert_eq!(row, decoded_row);
    }

    #[test]
    fn test_filter_by_scripthash_with_token() {
        let scripthash = ScriptHash::hash(&[42]);
        let txid = [1u8; 32];
        let row_with_token = ScriptHashIndexRow::new_funding(
            &scripthash,
            &OutPointHash::hash(&[11]),
            OutputFlags::FundingHasTokens,
            txid,
            0,
            42,
        )
        .to_row();
        let row_without_token = ScriptHashIndexRow::new_funding(
            &scripthash,
            &OutPointHash::hash(&[11]),
            OutputFlags::FundingNone,
            txid,
            0,
            42,
        )
        .to_row();

        let (filter_token_prefix, filter_token_bitmask) = ScriptHashIndexRow::filter_by_outputs(
            scripthash.into_inner(),
            &QueryFilter::filter_token_only(),
        );
        let (all_prefix, all_bitmask) =
            ScriptHashIndexRow::filter_by_outputs(scripthash.into_inner(), &QueryFilter::default());
        let (exclude_token_prefix, exclude_token_bitmask) = ScriptHashIndexRow::filter_by_outputs(
            scripthash.into_inner(),
            &QueryFilter::filter_exclude_tokens(),
        );

        // Check that the prefix (scripthash) matches the key start,
        // and the bitmask matches the flags byte at FLAGS_BYTE_OFFSET.
        let matches_filter = |row: &Row, prefix: &[u8], bitmask: Option<(u8, usize)>| -> bool {
            let key = row.key.to_vec();
            let prefix_matches = key.iter().zip(prefix.iter()).all(|(x, y)| x == y);
            if !prefix_matches {
                return false;
            }
            match bitmask {
                Some((mask, pos)) => key[pos] & mask != 0,
                None => true,
            }
        };

        // Token-only filter: should match row with token, not without
        assert!(matches_filter(
            &row_with_token,
            &filter_token_prefix,
            filter_token_bitmask
        ));
        assert!(!matches_filter(
            &row_without_token,
            &filter_token_prefix,
            filter_token_bitmask
        ));

        // All-outputs filter: should match both
        assert!(matches_filter(&row_with_token, &all_prefix, all_bitmask));
        assert!(matches_filter(&row_without_token, &all_prefix, all_bitmask));

        // Exclude-tokens filter: should match row without token, not with
        assert!(!matches_filter(
            &row_with_token,
            &exclude_token_prefix,
            exclude_token_bitmask
        ));
        assert!(matches_filter(
            &row_without_token,
            &exclude_token_prefix,
            exclude_token_bitmask
        ));
    }

    #[test]
    fn test_filter_prefix_encoding() {
        use crate::store::db_encode;

        // Test that encoding just the scripthash array DOES match
        // the first 32 bytes of the encoded struct key (encoding is correct)
        let scripthash = [0x42u8; 32];

        // Method 1: Encode just the scripthash array
        let encoded_array = db_encode(&scripthash).unwrap();

        // Method 2: Encode a dummy key and take first 32 bytes
        let dummy_key = ScriptHashIndexKey {
            scripthash,
            height_be: [0u8; 4],
            flags: 0,
            outpointhash: [0u8; 32],
        };
        let encoded_key = db_encode(&dummy_key).unwrap();
        let prefix_from_key = &encoded_key[0..32];

        // Method 3: Create an actual row and check its key prefix
        let row = ScriptHashIndexRow::new_funding(
            &ScriptHash::from_inner(scripthash),
            &OutPointHash::hash(&[11]),
            OutputFlags::FundingNone,
            [1u8; 32],
            0,
            42,
        );
        let row_key = row.to_row().key;
        let prefix_from_row = &row_key[0..32];

        // Verify: The encoded array DOES match the key prefix
        assert_eq!(
            encoded_array, prefix_from_key,
            "Encoding just the scripthash array should match the key prefix"
        );

        // Verify: The prefix from encoded key should match the actual row key prefix
        assert_eq!(
            prefix_from_key, prefix_from_row,
            "Prefix from encoded key should match actual row key prefix"
        );

        // Test token filter: when we use bitmask, does it match correctly?
        let (filter_with_flag_prefix, filter_with_flag_bitmask) =
            ScriptHashIndexRow::filter_by_outputs(scripthash, &QueryFilter::filter_token_only());

        // The prefix should be 32 bytes (scripthash)
        assert_eq!(filter_with_flag_prefix.len(), 32);
        assert_eq!(&filter_with_flag_prefix[0..32], prefix_from_key);

        // The bitmask should be set for FundingHasTokens at FLAGS_BYTE_OFFSET
        assert!(filter_with_flag_bitmask.is_some());
        let (bitmask, pos) = filter_with_flag_bitmask.unwrap();
        assert_eq!(pos, FLAGS_BYTE_OFFSET);
        assert_eq!(bitmask, OutputFlags::FundingHasTokens as u8);

        // For a row with the flag, the key at FLAGS_BYTE_OFFSET should match
        let row_with_flag = ScriptHashIndexRow::new_funding(
            &ScriptHash::from_inner(scripthash),
            &OutPointHash::hash(&[22]),
            OutputFlags::FundingHasTokens,
            [2u8; 32],
            0,
            43,
        );
        let row_with_flag_key = row_with_flag.to_row().key;
        // The prefix should match
        assert_eq!(&row_with_flag_key[0..32], &filter_with_flag_prefix[0..32]);
        // The bitmask should match the flag byte
        assert_eq!(row_with_flag_key[FLAGS_BYTE_OFFSET] & bitmask, bitmask);

        // For a row without the flag, key at FLAGS_BYTE_OFFSET should NOT match the bitmask
        assert_eq!(row_key[FLAGS_BYTE_OFFSET] & bitmask, 0);
    }
}
