use std::cell::RefCell;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};

use chrono::Utc;
use serde::{Deserialize, Serialize};

use super::backup::{BackupEntry, BackupManager};
use super::crypto;
use super::error::{ClosureError, Result};
use super::fsutil::atomic_write;
use super::provider::ProviderStore;

const STORE_FORMAT_VERSION: u32 = 1;
/// Files tracked in the manifest and captured by backup snapshots.
///
/// `manifest.json` is deliberately excluded — it is the record itself,
/// not a file the manifest should self-reference.
const TRACKED_FILES: [&str; 3] = [
    "providers.enc",
    "providers.json",
    "state.json",
];

/// A vault that manages encrypted and decrypted provider configuration files.
pub struct Store {
    workdata_dir: PathBuf,
    password: RefCell<Option<String>>,
}

impl Store {
    /// Creates a new `Store` rooted at the given `workdata_dir`.
    pub fn new(workdata_dir: &Path) -> Self {
        Self {
            workdata_dir: workdata_dir.to_path_buf(),
            password: RefCell::new(None),
        }
    }

    pub fn backup_manager(&self) -> BackupManager {
        BackupManager::new(&self.workdata_dir.join("backups"))
    }

    fn encrypted_path(&self) -> PathBuf {
        self.workdata_dir.join("providers.enc")
    }

    fn decrypted_path(&self) -> PathBuf {
        self.workdata_dir.join("providers.json")
    }

    fn state_path(&self) -> PathBuf {
        self.workdata_dir.join("state.json")
    }

    fn manifest_path(&self) -> PathBuf {
        self.workdata_dir.join("manifest.json")
    }

    /// Returns `true` if the decrypted provider file exists (user is authenticated).
    pub fn is_authenticated(&self) -> bool {
        self.decrypted_path().exists()
    }

    /// Returns `true` if the encrypted provider file exists on disk.
    pub fn has_encrypted(&self) -> bool {
        self.encrypted_path().exists()
    }

    /// Initializes a new vault with the given `password`, creating both encrypted and decrypted
    /// provider files. Returns an error if an encrypted file already exists.
    pub fn init(&self, password: &str) -> Result<ProviderStore> {
        if self.has_encrypted() {
            return Err(ClosureError::Crypto(
                "vault already exists — providers.enc already present. Use `closure auth` to unlock."
                    .to_string(),
            ));
        }
        fs::create_dir_all(&self.workdata_dir)?;
        self.password.replace(Some(password.to_string()));
        let store = ProviderStore::default();
        self.persist_store(&store, &HashMap::new(), false)?;
        Ok(store)
    }

    /// Loads the decrypted provider store from disk.
    pub fn load(&self) -> Result<ProviderStore> {
        let path = self.decrypted_path();
        if !path.exists() {
            return Err(ClosureError::AuthRequired);
        }
        let contents = fs::read_to_string(&path)?;
        let store = serde_json::from_str(&contents)?;
        Ok(store)
    }

    /// Saves the provider store to the decrypted file and re-encrypts the encrypted file if the
    /// password is cached.
    pub fn save(&self, store: &ProviderStore) -> Result<()> {
        let state = self.load_state()?;
        self.persist_store(store, &state, true)
    }

    /// Ensure the encryption password is available. If not cached, prompt
    /// the user on the terminal. Returns true if now available.
    pub fn ensure_password(&self) -> Result<bool> {
        if self.password.borrow().is_some() {
            return Ok(true);
        }
        if !self.has_encrypted() {
            return Ok(false);
        }
        let password = rpassword::prompt_password("Password: ")?;
        self.load_encrypted(&password)?;
        self.password.replace(Some(password));
        Ok(true)
    }

    /// Loads and decrypts the provider store from the encrypted file using the given `password`.
    pub fn load_encrypted(&self, password: &str) -> Result<ProviderStore> {
        let ciphertext = fs::read(self.encrypted_path())?;
        let plaintext = crypto::decrypt(&ciphertext, password)?;
        let store = serde_json::from_slice(&plaintext)?;
        Ok(store)
    }

    /// Authenticates with the given `password`, decrypting the vault, caching the password, and
    /// writing the decrypted provider file.
    pub fn authenticate(&self, password: &str) -> Result<ProviderStore> {
        let store = self.load_encrypted(password)?;
        self.password.replace(Some(password.to_string()));
        let state = self.load_state().unwrap_or_default();
        self.persist_store(&store, &state, true)?;
        Ok(store)
    }

    /// Logs out by removing the decrypted provider file and clearing the cached password.
    pub fn logout(&self) -> Result<()> {
        let path = self.decrypted_path();
        if path.exists() {
            fs::remove_file(path)?;
        }
        self.password.replace(None);
        Ok(())
    }

    /// Loads the per-agent active provider state from disk.
    pub fn load_state(&self) -> Result<HashMap<String, Option<String>>> {
        let path = self.state_path();
        if !path.exists() {
            return Ok(HashMap::new());
        }
        let contents = fs::read_to_string(&path)?;
        let state: HashMap<String, Option<String>> = serde_json::from_str(&contents)?;
        Ok(state)
    }

    /// Saves the per-agent active provider state to disk.
    pub fn save_state(&self, state: &HashMap<String, Option<String>>) -> Result<()> {
        let store = self.load().or_else(|err| {
            if matches!(err, ClosureError::AuthRequired) {
                Ok(ProviderStore::default())
            } else {
                Err(err)
            }
        })?;
        self.persist_store(&store, state, true)
    }

    pub fn verify(&self) -> Result<StoreHealth> {
        let mut issues = Vec::new();
        let encrypted_exists = self.encrypted_path().exists();
        let decrypted_exists = self.decrypted_path().exists();
        let state_exists = self.state_path().exists();
        let manifest_exists = self.manifest_path().exists();

        let decrypted_store = if decrypted_exists {
            match self.load() {
                Ok(store) => Some(store),
                Err(err) => {
                    issues.push(format!("decrypted store unreadable: {err}"));
                    None
                }
            }
        } else {
            None
        };

        if encrypted_exists {
            match self.read_manifest() {
                Ok(Some(manifest)) => {
                    if let Err(err) = self.validate_manifest(&manifest) {
                        issues.push(format!("manifest invalid: {err}"));
                    }
                }
                Ok(None) => {} // handled by the explicit check below
                Err(err) => issues.push(format!("manifest unreadable: {err}")),
            }
        }

        if encrypted_exists && !decrypted_exists {
            issues.push("vault locked: decrypted providers.json missing".to_string());
        }
        if decrypted_exists && !encrypted_exists {
            issues.push("decrypted providers.json exists without providers.enc".to_string());
        }
        if !state_exists {
            issues.push("state.json missing".to_string());
        }
        if encrypted_exists && !manifest_exists {
            issues.push("manifest.json missing".to_string());
        }

        if let Some(store) = decrypted_store {
            if store.version == 0 {
                issues.push("provider store version is invalid".to_string());
            }
        }

        Ok(StoreHealth {
            encrypted_exists,
            decrypted_exists,
            state_exists,
            manifest_exists,
            issues,
        })
    }

    pub fn list_backups(&self) -> Result<Vec<BackupEntry>> {
        self.backup_manager().list()
    }

    pub fn inspect_backup(&self, id: &str) -> Result<BackupEntry> {
        self.backup_manager().inspect(id)
    }

    pub fn restore_backup(&self, id: &str) -> Result<BackupEntry> {
        self.backup_manager().restore(id, &self.workdata_dir)
    }

    fn persist_store(
        &self,
        store: &ProviderStore,
        state: &HashMap<String, Option<String>>,
        with_backup: bool,
    ) -> Result<()> {
        fs::create_dir_all(&self.workdata_dir)?;
        if with_backup {
            let _ = self
                .backup_manager()
                .create_snapshot(&self.workdata_dir, &TRACKED_FILES)?;
        }

        let decrypted = serde_json::to_vec_pretty(store)?;
        atomic_write(&self.decrypted_path(), &decrypted)?;

        if let Some(ref pwd) = *self.password.borrow() {
            self.write_encrypted_bytes(&decrypted, pwd)?;
        } else if self.encrypted_path().exists() {
            return Err(ClosureError::AuthRequired);
        }

        let state_bytes = serde_json::to_vec_pretty(state)?;
        atomic_write(&self.state_path(), &state_bytes)?;
        self.write_manifest(store, state)?;
        Ok(())
    }

    fn write_encrypted_bytes(&self, plaintext: &[u8], password: &str) -> Result<()> {
        let ciphertext = crypto::encrypt(plaintext, password)?;
        atomic_write(&self.encrypted_path(), &ciphertext)
    }

    fn write_manifest(
        &self,
        store: &ProviderStore,
        state: &HashMap<String, Option<String>>,
    ) -> Result<()> {
        let manifest = StoreManifest {
            version: STORE_FORMAT_VERSION,
            snapshot_id: Utc::now()
                .timestamp_nanos_opt()
                .ok_or_else(|| {
                    ClosureError::Validation("failed to create snapshot id".to_string())
                })?
                .to_string(),
            created_at: Utc::now().to_rfc3339(),
            files: tracked_file_entries(&self.workdata_dir)?,
            provider_count: store.providers.len(),
            state_keys: state.keys().cloned().collect(),
        };
        let bytes = serde_json::to_vec_pretty(&manifest)?;
        atomic_write(&self.manifest_path(), &bytes)
    }

    fn read_manifest(&self) -> Result<Option<StoreManifest>> {
        let path = self.manifest_path();
        if !path.exists() {
            return Ok(None);
        }
        let contents = fs::read_to_string(path)?;
        let manifest = serde_json::from_str(&contents)?;
        Ok(Some(manifest))
    }

    fn validate_manifest(&self, manifest: &StoreManifest) -> Result<()> {
        if manifest.version == 0 {
            return Err(ClosureError::Validation(
                "manifest version must be > 0".to_string(),
            ));
        }
        for file in &manifest.files {
            let path = self.workdata_dir.join(&file.name);
            // providers.json is transient (removed on logout); skip instead
            // of erroring. Missing-file presence is reported by verify() itself.
            if !path.exists() {
                continue;
            }
            let bytes = fs::read(&path)?;
            let hash = super::sync::sha256_hex(&bytes);
            if hash != file.sha256 {
                return Err(ClosureError::Validation(format!(
                    "checksum mismatch for {}",
                    file.name
                )));
            }
        }
        Ok(())
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StoreHealth {
    pub encrypted_exists: bool,
    pub decrypted_exists: bool,
    pub state_exists: bool,
    pub manifest_exists: bool,
    pub issues: Vec<String>,
}

impl StoreHealth {
    pub fn is_healthy(&self) -> bool {
        self.issues.is_empty()
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct StoreManifest {
    version: u32,
    snapshot_id: String,
    created_at: String,
    files: Vec<TrackedFileEntry>,
    provider_count: usize,
    state_keys: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct TrackedFileEntry {
    name: String,
    size: u64,
    sha256: String,
}

fn tracked_file_entries(root: &Path) -> Result<Vec<TrackedFileEntry>> {
    let mut files = Vec::new();
    for rel in TRACKED_FILES {
        let path = root.join(rel);
        if !path.exists() {
            continue;
        }
        let bytes = fs::read(&path)?;
        files.push(TrackedFileEntry {
            name: rel.to_string(),
            size: bytes.len() as u64,
            sha256: super::sync::sha256_hex(&bytes),
        });
    }
    Ok(files)
}
