//! Persistence for shadow mode testing data.
//!
//! Stores shadow tests, matches, and decisions in JSONL files:
//! - `tests.jsonl` - All ShadowTest records
//! - `matches/<test_id>.jsonl` - Matches per test
//! - `decisions.jsonl` - Decision audit log

use std::collections::HashMap;
use std::fs::{self, File, OpenOptions};
use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};

use tracing::{debug, info, warn};
use uuid::Uuid;

use super::types::{MatchFeedback, ShadowDecision, ShadowMatch, ShadowStatus, ShadowTest};
use crate::AphoriaError;

/// Persistent store for shadow mode testing data.
pub struct ShadowStore {
    /// Base directory for shadow data.
    base_dir: PathBuf,
}

impl ShadowStore {
    /// Create a new shadow store.
    ///
    /// Creates the directory structure if it doesn't exist.
    pub fn new(base_dir: impl AsRef<Path>) -> Result<Self, AphoriaError> {
        let base_dir = base_dir.as_ref().to_path_buf();

        // Create directory structure
        fs::create_dir_all(&base_dir)
            .map_err(|e| AphoriaError::Shadow(format!("Failed to create shadow directory: {e}")))?;
        fs::create_dir_all(base_dir.join("matches")).map_err(|e| {
            AphoriaError::Shadow(format!("Failed to create matches directory: {e}"))
        })?;

        debug!(path = %base_dir.display(), "Opened shadow store");
        Ok(Self { base_dir })
    }

    // ========================================================================
    // Test operations
    // ========================================================================

    /// Save a shadow test.
    pub fn save_test(&self, test: &ShadowTest) -> Result<(), AphoriaError> {
        let mut tests = self.load_all_tests()?;

        // Update or insert
        tests.insert(test.id, test.clone());

        // Rewrite file
        self.write_tests(&tests)?;
        debug!(test_id = %test.id, "Saved shadow test");
        Ok(())
    }

    /// Get a shadow test by ID.
    pub fn get_test(&self, id: &Uuid) -> Result<Option<ShadowTest>, AphoriaError> {
        let tests = self.load_all_tests()?;
        Ok(tests.get(id).cloned())
    }

    /// Get a shadow test by extractor name.
    pub fn get_test_by_name(&self, name: &str) -> Result<Option<ShadowTest>, AphoriaError> {
        let tests = self.load_all_tests()?;
        Ok(tests.values().find(|t| t.extractor_name == name).cloned())
    }

    /// List all active shadow tests.
    pub fn list_active_tests(&self) -> Result<Vec<ShadowTest>, AphoriaError> {
        let tests = self.load_all_tests()?;
        Ok(tests.into_values().filter(|t| t.status == ShadowStatus::Active).collect())
    }

    /// List all shadow tests.
    pub fn list_all_tests(&self) -> Result<Vec<ShadowTest>, AphoriaError> {
        let tests = self.load_all_tests()?;
        Ok(tests.into_values().collect())
    }

    /// Delete a shadow test and its matches.
    pub fn delete_test(&self, id: &Uuid) -> Result<(), AphoriaError> {
        let mut tests = self.load_all_tests()?;
        tests.remove(id);
        self.write_tests(&tests)?;

        // Delete matches file
        let matches_path = self.matches_path(id);
        if matches_path.exists() {
            fs::remove_file(&matches_path)
                .map_err(|e| AphoriaError::Shadow(format!("Failed to delete matches file: {e}")))?;
        }

        info!(test_id = %id, "Deleted shadow test");
        Ok(())
    }

    // ========================================================================
    // Match operations
    // ========================================================================

    /// Save a shadow match.
    pub fn save_match(&self, match_: &ShadowMatch) -> Result<(), AphoriaError> {
        let path = self.matches_path(&match_.test_id);
        let mut file = OpenOptions::new()
            .create(true)
            .append(true)
            .open(&path)
            .map_err(|e| AphoriaError::Shadow(format!("Failed to open matches file: {e}")))?;

        let json = serde_json::to_string(match_)
            .map_err(|e| AphoriaError::Shadow(format!("Failed to serialize match: {e}")))?;

        writeln!(file, "{}", json)
            .map_err(|e| AphoriaError::Shadow(format!("Failed to write match: {e}")))?;

        debug!(match_id = %match_.id, test_id = %match_.test_id, "Saved shadow match");
        Ok(())
    }

    /// Get pending (unreviewed) matches for a test.
    pub fn get_pending_matches(&self, test_id: &Uuid) -> Result<Vec<ShadowMatch>, AphoriaError> {
        let matches = self.load_matches(test_id)?;
        Ok(matches.into_iter().filter(|m| !m.is_reviewed()).collect())
    }

    /// Get all matches for a test.
    pub fn get_all_matches(&self, test_id: &Uuid) -> Result<Vec<ShadowMatch>, AphoriaError> {
        self.load_matches(test_id)
    }

    /// Update feedback for a match.
    pub fn update_match_feedback(
        &self,
        test_id: &Uuid,
        match_id: &Uuid,
        feedback: MatchFeedback,
    ) -> Result<(), AphoriaError> {
        let mut matches = self.load_matches(test_id)?;

        let match_ = matches
            .iter_mut()
            .find(|m| &m.id == match_id)
            .ok_or_else(|| AphoriaError::Shadow(format!("Match {} not found", match_id)))?;

        match feedback {
            MatchFeedback::TruePositive => match_.mark_true_positive(),
            MatchFeedback::FalsePositive => match_.mark_false_positive(),
        }

        self.write_matches(test_id, &matches)?;
        debug!(match_id = %match_id, feedback = %feedback, "Updated match feedback");
        Ok(())
    }

    // ========================================================================
    // Decision operations
    // ========================================================================

    /// Log a shadow decision.
    pub fn log_decision(&self, decision: &ShadowDecision) -> Result<(), AphoriaError> {
        let path = self.decisions_path();
        let mut file = OpenOptions::new()
            .create(true)
            .append(true)
            .open(&path)
            .map_err(|e| AphoriaError::Shadow(format!("Failed to open decisions file: {e}")))?;

        let json = serde_json::to_string(decision)
            .map_err(|e| AphoriaError::Shadow(format!("Failed to serialize decision: {e}")))?;

        writeln!(file, "{}", json)
            .map_err(|e| AphoriaError::Shadow(format!("Failed to write decision: {e}")))?;

        info!(
            decision_id = %decision.id,
            kind = %decision.kind,
            extractor = %decision.extractor_name,
            "Logged shadow decision"
        );
        Ok(())
    }

    /// Get all decisions for a test.
    pub fn get_decisions(&self, test_id: &Uuid) -> Result<Vec<ShadowDecision>, AphoriaError> {
        let path = self.decisions_path();
        if !path.exists() {
            return Ok(vec![]);
        }

        let file = File::open(&path)
            .map_err(|e| AphoriaError::Shadow(format!("Failed to open decisions file: {e}")))?;

        let reader = BufReader::new(file);
        let mut decisions = Vec::new();

        for line in reader.lines() {
            let line =
                line.map_err(|e| AphoriaError::Shadow(format!("Failed to read line: {e}")))?;

            if line.trim().is_empty() {
                continue;
            }

            let decision: ShadowDecision = serde_json::from_str(&line)
                .map_err(|e| AphoriaError::Shadow(format!("Failed to parse decision: {e}")))?;

            if &decision.test_id == test_id {
                decisions.push(decision);
            }
        }

        Ok(decisions)
    }

    // ========================================================================
    // Internal helpers
    // ========================================================================

    fn tests_path(&self) -> PathBuf {
        self.base_dir.join("tests.jsonl")
    }

    fn matches_path(&self, test_id: &Uuid) -> PathBuf {
        self.base_dir.join("matches").join(format!("{}.jsonl", test_id))
    }

    fn decisions_path(&self) -> PathBuf {
        self.base_dir.join("decisions.jsonl")
    }

    fn load_all_tests(&self) -> Result<HashMap<Uuid, ShadowTest>, AphoriaError> {
        let path = self.tests_path();
        if !path.exists() {
            return Ok(HashMap::new());
        }

        let file = File::open(&path)
            .map_err(|e| AphoriaError::Shadow(format!("Failed to open tests file: {e}")))?;

        let reader = BufReader::new(file);
        let mut tests = HashMap::new();

        for line in reader.lines() {
            let line =
                line.map_err(|e| AphoriaError::Shadow(format!("Failed to read line: {e}")))?;

            if line.trim().is_empty() {
                continue;
            }

            match serde_json::from_str::<ShadowTest>(&line) {
                Ok(test) => {
                    tests.insert(test.id, test);
                }
                Err(e) => {
                    warn!(error = %e, "Failed to parse shadow test, skipping");
                }
            }
        }

        Ok(tests)
    }

    fn write_tests(&self, tests: &HashMap<Uuid, ShadowTest>) -> Result<(), AphoriaError> {
        let path = self.tests_path();
        let mut file = File::create(&path)
            .map_err(|e| AphoriaError::Shadow(format!("Failed to create tests file: {e}")))?;

        for test in tests.values() {
            let json = serde_json::to_string(test)
                .map_err(|e| AphoriaError::Shadow(format!("Failed to serialize test: {e}")))?;
            writeln!(file, "{}", json)
                .map_err(|e| AphoriaError::Shadow(format!("Failed to write test: {e}")))?;
        }

        Ok(())
    }

    fn load_matches(&self, test_id: &Uuid) -> Result<Vec<ShadowMatch>, AphoriaError> {
        let path = self.matches_path(test_id);
        if !path.exists() {
            return Ok(vec![]);
        }

        let file = File::open(&path)
            .map_err(|e| AphoriaError::Shadow(format!("Failed to open matches file: {e}")))?;

        let reader = BufReader::new(file);
        let mut matches = Vec::new();

        for line in reader.lines() {
            let line =
                line.map_err(|e| AphoriaError::Shadow(format!("Failed to read line: {e}")))?;

            if line.trim().is_empty() {
                continue;
            }

            match serde_json::from_str::<ShadowMatch>(&line) {
                Ok(m) => matches.push(m),
                Err(e) => {
                    warn!(error = %e, "Failed to parse shadow match, skipping");
                }
            }
        }

        Ok(matches)
    }

    fn write_matches(&self, test_id: &Uuid, matches: &[ShadowMatch]) -> Result<(), AphoriaError> {
        let path = self.matches_path(test_id);
        let mut file = File::create(&path)
            .map_err(|e| AphoriaError::Shadow(format!("Failed to create matches file: {e}")))?;

        for m in matches {
            let json = serde_json::to_string(m)
                .map_err(|e| AphoriaError::Shadow(format!("Failed to serialize match: {e}")))?;
            writeln!(file, "{}", json)
                .map_err(|e| AphoriaError::Shadow(format!("Failed to write match: {e}")))?;
        }

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::TempDir;

    fn create_test_store() -> (ShadowStore, TempDir) {
        let temp = TempDir::new().expect("temp dir");
        let store = ShadowStore::new(temp.path()).expect("create store");
        (store, temp)
    }

    #[test]
    fn test_save_and_get_test() {
        let (store, _temp) = create_test_store();

        let test = ShadowTest::new(
            "test_extractor".to_string(),
            PathBuf::from("/path/to/extractor.yaml"),
            Uuid::new_v4(),
        );

        store.save_test(&test).expect("save");
        let loaded = store.get_test(&test.id).expect("get").expect("exists");

        assert_eq!(loaded.id, test.id);
        assert_eq!(loaded.extractor_name, "test_extractor");
    }

    #[test]
    fn test_get_test_by_name() {
        let (store, _temp) = create_test_store();

        let test = ShadowTest::new(
            "my_extractor".to_string(),
            PathBuf::from("/path/to/extractor.yaml"),
            Uuid::new_v4(),
        );

        store.save_test(&test).expect("save");
        let loaded = store.get_test_by_name("my_extractor").expect("get").expect("exists");

        assert_eq!(loaded.id, test.id);
    }

    #[test]
    fn test_list_active_tests() {
        let (store, _temp) = create_test_store();

        // Create active test
        let active = ShadowTest::new(
            "active_extractor".to_string(),
            PathBuf::from("/path/active.yaml"),
            Uuid::new_v4(),
        );
        store.save_test(&active).expect("save");

        // Create graduated test
        let mut graduated = ShadowTest::new(
            "graduated_extractor".to_string(),
            PathBuf::from("/path/graduated.yaml"),
            Uuid::new_v4(),
        );
        graduated.graduate();
        store.save_test(&graduated).expect("save");

        let active_tests = store.list_active_tests().expect("list");
        assert_eq!(active_tests.len(), 1);
        assert_eq!(active_tests[0].extractor_name, "active_extractor");
    }

    #[test]
    fn test_delete_test() {
        let (store, _temp) = create_test_store();

        let test = ShadowTest::new(
            "test_extractor".to_string(),
            PathBuf::from("/path/to/extractor.yaml"),
            Uuid::new_v4(),
        );

        store.save_test(&test).expect("save");
        assert!(store.get_test(&test.id).expect("get").is_some());

        store.delete_test(&test.id).expect("delete");
        assert!(store.get_test(&test.id).expect("get").is_none());
    }

    #[test]
    fn test_save_and_get_matches() {
        let (store, _temp) = create_test_store();

        let test = ShadowTest::new(
            "test_extractor".to_string(),
            PathBuf::from("/path/to/extractor.yaml"),
            Uuid::new_v4(),
        );
        store.save_test(&test).expect("save");

        let match1 = ShadowMatch::new(
            test.id,
            PathBuf::from("/file1.py"),
            10,
            "verify_ssl = false".to_string(),
            "context1".to_string(),
        );
        let match2 = ShadowMatch::new(
            test.id,
            PathBuf::from("/file2.py"),
            20,
            "verify_ssl = false".to_string(),
            "context2".to_string(),
        );

        store.save_match(&match1).expect("save match 1");
        store.save_match(&match2).expect("save match 2");

        let pending = store.get_pending_matches(&test.id).expect("get pending");
        assert_eq!(pending.len(), 2);
    }

    #[test]
    fn test_update_match_feedback() {
        let (store, _temp) = create_test_store();

        let test = ShadowTest::new(
            "test_extractor".to_string(),
            PathBuf::from("/path/to/extractor.yaml"),
            Uuid::new_v4(),
        );
        store.save_test(&test).expect("save");

        let match_ = ShadowMatch::new(
            test.id,
            PathBuf::from("/file.py"),
            10,
            "verify_ssl = false".to_string(),
            "context".to_string(),
        );
        store.save_match(&match_).expect("save match");

        store
            .update_match_feedback(&test.id, &match_.id, MatchFeedback::TruePositive)
            .expect("update");

        let pending = store.get_pending_matches(&test.id).expect("get pending");
        assert_eq!(pending.len(), 0); // No longer pending

        let all = store.get_all_matches(&test.id).expect("get all");
        assert_eq!(all.len(), 1);
        assert_eq!(all[0].feedback, Some(MatchFeedback::TruePositive));
    }

    #[test]
    fn test_log_and_get_decisions() {
        let (store, _temp) = create_test_store();

        let test = ShadowTest::new(
            "test_extractor".to_string(),
            PathBuf::from("/path/to/extractor.yaml"),
            Uuid::new_v4(),
        );
        store.save_test(&test).expect("save");

        let decision = ShadowDecision::entered_shadow(&test);
        store.log_decision(&decision).expect("log");

        let decisions = store.get_decisions(&test.id).expect("get");
        assert_eq!(decisions.len(), 1);
        assert_eq!(decisions[0].test_id, test.id);
    }
}
