use anyhow::{Context, Result};
use redb::{Database, ReadableDatabase, ReadableTable, TableDefinition, WriteTransaction};
use serde::{Deserialize, Serialize};
use std::fs::{self, File};
use std::io::Cursor;
use std::path::Path;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};

// --- Synced from Vegh 0.4.0 src/storage.rs ---

pub const CACHE_DIR: &str = ".veghcache";
const CACHE_DB_FILE: &str = "cache.redb";
const JSON_CACHE_FILE: &str = "index.json";

// Redb Tables - Single Table Schema (V3)
const TABLE_DATA_V3: TableDefinition<&str, &[u8]> = TableDefinition::new("data_v3");
const TABLE_INODES_V3: TableDefinition<u64, &str> = TableDefinition::new("inodes_v3");

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct StoredChunk {
    pub hash: [u8; 32],
    pub offset: u64,
    pub length: u32,
}

// --- MISSING STRUCTS ADDED HERE ---
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct ManifestEntry {
    pub path: String,
    pub hash: String,
    pub size: u64,
    pub modified: u64,
    pub mode: u32,
    pub chunks: Option<Vec<String>>,
}

#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct SnapshotManifest {
    pub entries: Vec<ManifestEntry>,
}
// ----------------------------------

// Cache Entry Structure
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct FileCacheEntry {
    pub size: u64,
    pub modified: u64,

    #[serde(default)]
    pub ctime_sec: i64,
    #[serde(default)]
    pub ctime_nsec: u32,
    #[serde(default)]
    pub device_id: u64,
    #[serde(default)]
    pub inode: u64,

    #[serde(default)]
    pub last_seen: u64,

    pub hash: Option<[u8; 32]>,

    // Compressed Chunks (Now stores serialized Vec<StoredChunk>)
    #[serde(default)]
    pub chunks_compressed: Option<Vec<u8>>,

    #[serde(default)]
    pub sparse_hash: Option<[u8; 32]>,
}

impl FileCacheEntry {
    pub fn set_chunks(&mut self, chunks: Vec<StoredChunk>) -> Result<()> {
        let serialized = bincode::serialize(&chunks)?;
        // Compress using zstd level 3
        let compressed = zstd::stream::encode_all(Cursor::new(serialized), 3)?;
        self.chunks_compressed = Some(compressed);
        Ok(())
    }

    pub fn get_chunks(&self) -> Result<Option<Vec<StoredChunk>>> {
        if let Some(compressed) = &self.chunks_compressed {
            let decompressed = zstd::stream::decode_all(Cursor::new(compressed))?;

            if let Ok(chunks) = bincode::deserialize::<Vec<StoredChunk>>(&decompressed) {
                return Ok(Some(chunks));
            }

            // Legacy handling or corruption -> return None to force recompute
            if decompressed.len() % 32 == 0 {
                return Ok(None);
            }
            Ok(None)
        } else {
            Ok(None)
        }
    }
}

// Helper struct for legacy migration
#[derive(Serialize, Deserialize, Debug, Default)]
struct LegacyVeghCache {
    pub last_snapshot: i64,
    pub files: std::collections::HashMap<String, LegacyFileCacheEntry>,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
struct LegacyFileCacheEntry {
    pub size: u64,
    pub modified: u64,
    pub inode: u64,
    pub hash: Option<String>,
    pub chunks: Option<Vec<String>>,
    pub sparse_hash: Option<String>,
}

// Reader Handle
#[derive(Clone)]
pub struct CacheReader {
    db: Arc<Database>,
}

impl CacheReader {
    pub fn get(&self, path: &str) -> Result<Option<FileCacheEntry>> {
        // Requires usage of ReadableDatabase trait which is now imported
        let txn = self.db.begin_read()?;
        if let Ok(table) = txn.open_table(TABLE_DATA_V3) {
            // Explicitly handle the Option returned by table.get
            if let Some(v) = table.get(path)? {
                let entry: FileCacheEntry = bincode::deserialize(v.value())?;
                return Ok(Some(entry));
            }
        }
        Ok(None)
    }
}

pub struct CacheDB {
    db: Arc<Database>,
    txn: Option<WriteTransaction>,
}

impl CacheDB {
    pub fn open(source: &Path) -> Result<Self> {
        let cache_dir = source.join(CACHE_DIR);
        if !cache_dir.exists() {
            fs::create_dir(&cache_dir).context("Failed to create cache dir")?;
            let gitignore_path = cache_dir.join(".gitignore");
            if !gitignore_path.exists() {
                let content = "# Generated by Vegh\n*\n";
                let _ = fs::write(gitignore_path, content);
            }
        }

        let db_path = cache_dir.join(CACHE_DB_FILE);
        let db = Database::create(&db_path)?;
        let db = Arc::new(db);

        let mut cache_db = Self {
            db: db.clone(),
            txn: Some(db.begin_write()?),
        };

        // Check legacy JSON and migrate if needed (One-time migration for old PyVegh users)
        let json_path = cache_dir.join(JSON_CACHE_FILE);
        if json_path.exists() {
            cache_db.migrate_legacy_json(&json_path)?;
        }

        Ok(cache_db)
    }

    pub fn reader(&self) -> CacheReader {
        CacheReader {
            db: self.db.clone(),
        }
    }

    fn migrate_legacy_json(&mut self, path: &Path) -> Result<()> {
        if let Ok(file) = File::open(path)
            && let Ok(cache) = serde_json::from_reader::<_, LegacyVeghCache>(file) {
                let now = SystemTime::now()
                    .duration_since(UNIX_EPOCH)
                    .unwrap()
                    .as_secs();
                let txn = self.txn.as_mut().unwrap();

                let mut data = txn.open_table(TABLE_DATA_V3)?;
                let mut inodes = txn.open_table(TABLE_INODES_V3)?;

                for (k, v) in cache.files {
                    let hash_bytes = v
                        .hash
                        .and_then(|h| hex::decode(h).ok())
                        .and_then(|v| v.try_into().ok());

                    let new_entry = FileCacheEntry {
                        size: v.size,
                        modified: v.modified,
                        inode: v.inode,
                        ctime_sec: 0,
                        ctime_nsec: 0,
                        device_id: 0,
                        last_seen: now,
                        hash: hash_bytes,
                        chunks_compressed: None, // Force re-chunking
                        sparse_hash: None,
                    };

                    let bytes = bincode::serialize(&new_entry)?;
                    data.insert(k.as_str(), bytes.as_slice())?;
                    if new_entry.inode > 0 {
                        inodes.insert(new_entry.inode, k.as_str())?;
                    }
                }
            }
        // Remove legacy file after migration
        let _ = fs::remove_file(path);
        Ok(())
    }

    pub fn insert(&mut self, path: &str, entry: &FileCacheEntry) -> Result<()> {
        let txn = self.txn.as_mut().unwrap();
        let bytes = bincode::serialize(entry)?;

        let mut data = txn.open_table(TABLE_DATA_V3)?;
        data.insert(path, bytes.as_slice())?;

        if entry.inode > 0 {
            let mut inodes = txn.open_table(TABLE_INODES_V3)?;
            inodes.insert(entry.inode, path)?;
        }
        Ok(())
    }

    pub fn commit_batch(&mut self) -> Result<()> {
        if let Some(txn) = self.txn.take() {
            txn.commit()?;
            self.txn = Some(self.db.begin_write()?);
        }
        Ok(())
    }

    pub fn garbage_collect(&mut self, retention_seconds: u64) -> Result<u64> {
        let now = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_secs();
        let mut keys_to_remove: Vec<(String, u64)> = Vec::new();

        {
            let txn = self.txn.as_ref().unwrap();
            if let Ok(table) = txn.open_table(TABLE_DATA_V3) {
                // Iterating over the table needs careful handling of Result
                for res in table.iter()? {
                    let (k, v) = res?;
                    if let Ok(entry) = bincode::deserialize::<FileCacheEntry>(v.value())
                        && now.saturating_sub(entry.last_seen) >= retention_seconds {
                            keys_to_remove.push((k.value().to_string(), entry.inode));
                        }
                }
            }
        }

        let count = keys_to_remove.len() as u64;
        if count > 0 {
            let txn = self.txn.as_mut().unwrap();
            let mut data = txn.open_table(TABLE_DATA_V3)?;
            let mut inodes = txn.open_table(TABLE_INODES_V3)?;

            for (path, inode) in keys_to_remove {
                data.remove(path.as_str())?;
                if inode > 0 {
                    inodes.remove(inode)?;
                }
            }
        }

        Ok(count)
    }

    pub fn commit(mut self) -> Result<()> {
        if let Some(txn) = self.txn.take() {
            txn.commit()?;
        }
        Ok(())
    }
}
