use std::path::Path;

use redb::{Database, ReadableTable, TableDefinition};
use redb::ReadableDatabase;

use crate::error::Result;
use crate::types::INDEX_RECORD_SIZE;

// ── IndexRecord ──────────────────────────────────────────────────────────────

/// On-disk format: 52 bytes per record + 4 bytes CRC = 56 bytes total.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IndexRecord {
    pub key: [u8; 32],
    pub segment_id: u32,
    pub offset: u64,
    pub data_size: u32,
    pub flags: u8,
}

impl IndexRecord {
    pub fn new(key: [u8; 32], segment_id: u32, offset: u64, data_size: u32, flags: u8) -> Self {
        Self {
            key,
            segment_id,
            offset,
            data_size,
            flags,
        }
    }

    pub fn is_tombstone(&self) -> bool {
        self.flags == 1
    }

    pub fn encode(&self) -> [u8; INDEX_RECORD_SIZE] {
        let mut buf = [0u8; INDEX_RECORD_SIZE];
        buf[0..32].copy_from_slice(&self.key);
        buf[32..36].copy_from_slice(&self.segment_id.to_le_bytes());
        buf[36..44].copy_from_slice(&self.offset.to_le_bytes());
        buf[44..48].copy_from_slice(&self.data_size.to_le_bytes());
        buf[48] = self.flags;
        let crc = crate::checksum::crc32(&buf[..52]);
        buf[52..56].copy_from_slice(&crc.to_le_bytes());
        buf
    }

    pub fn decode(buf: &[u8; INDEX_RECORD_SIZE]) -> crate::error::Result<Self> {
        let stored_crc = u32::from_le_bytes(buf[52..56].try_into().unwrap());
        let computed = crate::checksum::crc32(&buf[..52]);
        if stored_crc != computed {
            return Err(crate::error::Error::BucketIndexCorrupt {
                path: std::path::PathBuf::new(),
                reason: format!(
                    "CRC mismatch: stored=0x{:08X} computed=0x{:08X}",
                    stored_crc, computed
                ),
            });
        }
        let mut key = [0u8; 32];
        key.copy_from_slice(&buf[0..32]);
        let segment_id = u32::from_le_bytes(buf[32..36].try_into().unwrap());
        let offset = u64::from_le_bytes(buf[36..44].try_into().unwrap());
        let data_size = u32::from_le_bytes(buf[44..48].try_into().unwrap());
        let flags = buf[48];
        Ok(Self {
            key,
            segment_id,
            offset,
            data_size,
            flags,
        })
    }
}

// ── redb Value impl for fixed-size record bytes ──────────────────────────────

/// Newtype wrapper so we can implement `redb::Value` for `[u8; INDEX_RECORD_SIZE]`.
#[derive(Debug, Clone, Copy)]
struct RecordBytes([u8; INDEX_RECORD_SIZE]);

impl redb::Value for RecordBytes {
    type SelfType<'a> = RecordBytes;
    type AsBytes<'a> = [u8; INDEX_RECORD_SIZE];

    fn fixed_width() -> Option<usize> {
        Some(INDEX_RECORD_SIZE)
    }

    fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a>
    where
        Self: 'a
    {
        let mut arr = [0u8; INDEX_RECORD_SIZE];
        arr.copy_from_slice(data);
        RecordBytes(arr)
    }

    fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Self::AsBytes<'a> {
        value.0
    }

    fn type_name() -> redb::TypeName {
        redb::TypeName::new("IndexRecord")
    }
}

// ── IndexStore ───────────────────────────────────────────────────────────────

const INDEX_TABLE: TableDefinition<[u8; 32], RecordBytes> = TableDefinition::new("blob_index");

/// Zero-heap key-value index backed by redb.
///
/// B-tree + mmap + ACID.  No manual compact / sort / dedup / per-bucket mmap
/// management.  Startup is O(1) — redb reads only its root page.
pub struct IndexStore {
    db: Database,
}

impl IndexStore {
    /// Open (or create) the index database at `dir/index.redb`.
    pub fn open(dir: &Path) -> Result<Self> {
        let path = dir.join("index.redb");
        let db = Database::create(&path)
            .map_err(|e| crate::error::Error::IndexDb(format!("failed to create index: {}", e)))?;
        // Ensure the table exists so reads on a fresh database don't fail.
        {
            let txn = db
                .begin_write()
                .map_err(|e| crate::error::Error::IndexDb(format!("init write txn: {}", e)))?;
            txn.open_table(INDEX_TABLE)
                .map_err(|e| crate::error::Error::IndexDb(format!("init table: {}", e)))?;
            txn.commit()
                .map_err(|e| crate::error::Error::IndexDb(format!("init commit: {}", e)))?;
        }
        Ok(Self { db })
    }

    /// Look up a key. Returns the latest IndexRecord, or None if absent/tombstone.
    pub fn get(&self, key: &[u8; 32]) -> Result<Option<IndexRecord>> {
        let txn = self
            .db
            .begin_read()
            .map_err(|e| crate::error::Error::IndexDb(format!("read txn: {}", e)))?;
        let table = txn
            .open_table(INDEX_TABLE)
            .map_err(|e| crate::error::Error::IndexDb(format!("open table: {}", e)))?;

        match table
            .get(key)
            .map_err(|e| crate::error::Error::IndexDb(format!("get: {}", e)))?
        {
            Some(guard) => {
                let record = IndexRecord::decode(&guard.value().0)?;
                Ok(if record.is_tombstone() { None } else { Some(record) })
            }
            None => Ok(None),
        }
    }

    /// Check whether a key exists (non-tombstone) in the store.
    pub fn exists(&self, key: &[u8; 32]) -> Result<bool> {
        self.get(key).map(|r| r.is_some())
    }

    /// Insert or update a record for a key.  Committed in a single write txn.
    pub fn insert(&self, record: &IndexRecord) -> Result<()> {
        let txn = self
            .db
            .begin_write()
            .map_err(|e| crate::error::Error::IndexDb(format!("write txn: {}", e)))?;
        {
            let mut table = txn
                .open_table(INDEX_TABLE)
                .map_err(|e| crate::error::Error::IndexDb(format!("open table: {}", e)))?;
            table
                .insert(&record.key, RecordBytes(record.encode()))
                .map_err(|e| crate::error::Error::IndexDb(format!("insert: {}", e)))?;
        }
        txn.commit()
            .map_err(|e| crate::error::Error::IndexDb(format!("commit: {}", e)))?;
        Ok(())
    }

    /// Batch insert multiple records in a single write transaction.
    pub fn insert_batch(&self, records: &[IndexRecord]) -> Result<()> {
        if records.is_empty() {
            return Ok(());
        }
        let txn = self
            .db
            .begin_write()
            .map_err(|e| crate::error::Error::IndexDb(format!("write txn: {}", e)))?;
        {
            let mut table = txn
                .open_table(INDEX_TABLE)
                .map_err(|e| crate::error::Error::IndexDb(format!("open table: {}", e)))?;
            for record in records {
                table
                    .insert(&record.key, RecordBytes(record.encode()))
                    .map_err(|e| crate::error::Error::IndexDb(format!("insert: {}", e)))?;
            }
        }
        txn.commit()
            .map_err(|e| crate::error::Error::IndexDb(format!("commit: {}", e)))?;
        Ok(())
    }

    /// Remove keys from the index in a single write transaction.
    pub fn delete_batch(&self, keys: &[[u8; 32]]) -> Result<()> {
        if keys.is_empty() {
            return Ok(());
        }
        let txn = self
            .db
            .begin_write()
            .map_err(|e| crate::error::Error::IndexDb(format!("write txn: {}", e)))?;
        {
            let mut table = txn
                .open_table(INDEX_TABLE)
                .map_err(|e| crate::error::Error::IndexDb(format!("open table: {}", e)))?;
            for key in keys {
                table
                    .remove(key)
                    .map_err(|e| crate::error::Error::IndexDb(format!("remove: {}", e)))?;
            }
        }
        txn.commit()
            .map_err(|e| crate::error::Error::IndexDb(format!("commit: {}", e)))?;
        Ok(())
    }

    /// Total number of live (non-tombstone) keys.
    pub fn total_keys(&self) -> Result<usize> {
        let txn = self
            .db
            .begin_read()
            .map_err(|e| crate::error::Error::IndexDb(format!("read txn: {}", e)))?;
        let table = txn
            .open_table(INDEX_TABLE)
            .map_err(|e| crate::error::Error::IndexDb(format!("open table: {}", e)))?;

        let mut count = 0usize;
        let iter = table
            .iter()
            .map_err(|e| crate::error::Error::IndexDb(format!("iter: {}", e)))?;
        for item in iter {
            let (_, guard) =
                item.map_err(|e| crate::error::Error::IndexDb(format!("iter next: {}", e)))?;
            let record = IndexRecord::decode(&guard.value().0)?;
            if !record.is_tombstone() {
                count += 1;
            }
        }
        Ok(count)
    }
}
