//! Fixture format and loader for LLM prompt evaluation.
//!
//! Fixtures are TOML files containing:
//! - Input code to analyze
//! - Expected claims (must_contain, must_not_contain)
//! - Metadata (category, language, difficulty)
//!
//! # Example Fixture
//!
//! ```toml
//! [metadata]
//! id = "tls-001"
//! name = "TLS verification disabled"
//! category = "tls"
//! language = "python"
//!
//! [input]
//! content = "requests.get(url, verify=False)"
//!
//! [expected]
//! must_contain = [
//!     { subject = "tls/cert_verification", predicate = "enabled", value = false }
//! ]
//! ```

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

use serde::{Deserialize, Serialize};
use tracing::{debug, instrument, warn};

use crate::error::{AphoriaError, Result};

/// A test fixture for evaluating LLM extraction.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Fixture {
    /// Fixture metadata.
    pub metadata: FixtureMetadata,

    /// Input to analyze.
    pub input: FixtureInput,

    /// Expected extraction results.
    pub expected: FixtureExpected,

    /// Scoring configuration.
    #[serde(default)]
    pub scoring: FixtureScoring,
}

/// Metadata about a fixture.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FixtureMetadata {
    /// Unique identifier (e.g., "tls-001").
    pub id: String,

    /// Human-readable name.
    pub name: String,

    /// Category (e.g., "tls", "jwt", "secrets").
    pub category: String,

    /// Programming language of the input.
    pub language: String,

    /// Difficulty level.
    #[serde(default = "default_difficulty")]
    pub difficulty: String,

    /// How this fixture was created.
    #[serde(default = "default_source")]
    pub source: String,

    /// Creation date (YYYY-MM-DD).
    #[serde(default)]
    pub created: Option<String>,

    /// Last update date (YYYY-MM-DD).
    #[serde(default)]
    pub updated: Option<String>,

    /// Optional notes about this fixture.
    #[serde(default)]
    pub notes: Option<String>,
}

fn default_difficulty() -> String {
    "medium".to_string()
}

fn default_source() -> String {
    "hand-curated".to_string()
}

/// Input for a fixture.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FixtureInput {
    /// Filename to use for the input (affects language detection).
    #[serde(default = "default_filename")]
    pub filename: String,

    /// The code content to analyze.
    pub content: String,
}

fn default_filename() -> String {
    "input.txt".to_string()
}

/// Expected extraction results.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FixtureExpected {
    /// Claims that MUST be extracted (recall test).
    #[serde(default)]
    pub must_contain: Vec<ExpectedClaim>,

    /// Claims that MUST NOT be extracted (precision test).
    #[serde(default)]
    pub must_not_contain: Vec<ExpectedClaim>,

    /// Optional: acceptable alternate formulations.
    #[serde(default)]
    pub acceptable_variants: Vec<ExpectedClaim>,
}

/// An expected claim for matching.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExpectedClaim {
    /// Subject path (e.g., "tls/cert_verification").
    pub subject: String,

    /// Predicate (e.g., "enabled").
    pub predicate: String,

    /// Expected value.
    pub value: serde_json::Value,

    /// Minimum confidence required (optional).
    #[serde(default)]
    pub min_confidence: Option<f32>,

    /// Rationale for this expectation (shown on failure).
    #[serde(default)]
    pub rationale: Option<String>,
}

/// Scoring configuration for a fixture.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FixtureScoring {
    /// Weight multiplier for this fixture's contribution to metrics.
    #[serde(default = "default_weight")]
    pub weight: f64,

    /// Expected minimum confidence from LLM.
    #[serde(default = "default_min_confidence")]
    pub min_confidence: f32,
}

fn default_weight() -> f64 {
    1.0
}

fn default_min_confidence() -> f32 {
    0.7
}

impl Default for FixtureScoring {
    fn default() -> Self {
        Self { weight: default_weight(), min_confidence: default_min_confidence() }
    }
}

/// Manifest for a fixture corpus.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CorpusManifest {
    /// Corpus metadata.
    pub corpus: CorpusMetadata,

    /// Category information.
    #[serde(default)]
    pub categories: HashMap<String, CategoryInfo>,

    /// Baseline metrics.
    #[serde(default)]
    pub baseline: Option<BaselineMetrics>,
}

/// Corpus metadata.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CorpusMetadata {
    /// Semantic version of the corpus.
    pub version: String,

    /// Creation date.
    #[serde(default)]
    pub created: Option<String>,

    /// Description of the corpus.
    #[serde(default)]
    pub description: Option<String>,
}

/// Information about a category.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CategoryInfo {
    /// Number of fixtures in this category.
    #[serde(default)]
    pub fixtures: usize,

    /// Description of this category.
    #[serde(default)]
    pub description: Option<String>,
}

/// Baseline metrics stored in the manifest.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BaselineMetrics {
    /// Precision (TP / (TP + FP)).
    pub precision: f64,

    /// Recall (TP / (TP + FN)).
    pub recall: f64,

    /// F1 score.
    pub f1: f64,

    /// Total fixtures in the baseline run.
    pub total_fixtures: usize,

    /// Prompt version used.
    pub prompt_version: String,

    /// Model used.
    pub model: String,

    /// When this baseline was measured.
    pub measured_at: String,
}

/// Loads fixtures from a directory.
pub struct FixtureLoader {
    /// Root directory containing fixtures.
    root: PathBuf,
}

impl FixtureLoader {
    /// Create a new fixture loader.
    pub fn new<P: AsRef<Path>>(root: P) -> Self {
        Self { root: root.as_ref().to_path_buf() }
    }

    /// Load the corpus manifest.
    #[instrument(skip(self), fields(root = %self.root.display()))]
    pub fn load_manifest(&self) -> Result<CorpusManifest> {
        let manifest_path = self.root.join("manifest.toml");

        if !manifest_path.exists() {
            return Err(AphoriaError::Config(format!(
                "Manifest not found at {}",
                manifest_path.display()
            )));
        }

        let content = fs::read_to_string(&manifest_path)
            .map_err(|e| AphoriaError::Config(format!("Failed to read manifest: {}", e)))?;

        let manifest: CorpusManifest = toml::from_str(&content)
            .map_err(|e| AphoriaError::Config(format!("Failed to parse manifest: {}", e)))?;

        debug!(version = %manifest.corpus.version, "Loaded corpus manifest");
        Ok(manifest)
    }

    /// Load all fixtures, optionally filtered by categories.
    #[instrument(skip(self), fields(root = %self.root.display()))]
    pub fn load_all(&self, categories: Option<&[String]>) -> Result<Vec<Fixture>> {
        let mut fixtures = Vec::new();

        // Walk the directory tree
        for entry in fs::read_dir(&self.root)
            .map_err(|e| AphoriaError::Config(format!("Failed to read fixtures dir: {}", e)))?
        {
            let entry =
                entry.map_err(|e| AphoriaError::Config(format!("Failed to read entry: {}", e)))?;

            let path = entry.path();

            if path.is_dir() {
                let dir_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");

                // Skip hidden directories and check category filter
                if dir_name.starts_with('.') {
                    continue;
                }

                if let Some(cats) = categories {
                    if !cats.iter().any(|c| c == dir_name) {
                        continue;
                    }
                }

                // Load fixtures from this category
                for fixture in self.load_category(&path)? {
                    fixtures.push(fixture);
                }
            } else if path.extension().map(|e| e == "toml").unwrap_or(false) {
                // Single fixture in root (not in a category)
                if path.file_name().map(|n| n != "manifest.toml").unwrap_or(false) {
                    if let Some(fixture) = self.load_fixture(&path)? {
                        fixtures.push(fixture);
                    }
                }
            }
        }

        debug!(count = fixtures.len(), "Loaded fixtures");
        Ok(fixtures)
    }

    /// Load fixtures from a category directory.
    fn load_category(&self, category_path: &Path) -> Result<Vec<Fixture>> {
        let mut fixtures = Vec::new();

        for entry in fs::read_dir(category_path)
            .map_err(|e| AphoriaError::Config(format!("Failed to read category dir: {}", e)))?
        {
            let entry =
                entry.map_err(|e| AphoriaError::Config(format!("Failed to read entry: {}", e)))?;

            let path = entry.path();

            if path.extension().map(|e| e == "toml").unwrap_or(false) {
                if let Some(fixture) = self.load_fixture(&path)? {
                    fixtures.push(fixture);
                }
            }
        }

        Ok(fixtures)
    }

    /// Load a single fixture from a file.
    #[instrument(skip(self), fields(path = %path.display()))]
    pub fn load_fixture(&self, path: &Path) -> Result<Option<Fixture>> {
        let content = fs::read_to_string(path)
            .map_err(|e| AphoriaError::Config(format!("Failed to read fixture: {}", e)))?;

        match toml::from_str::<Fixture>(&content) {
            Ok(fixture) => {
                debug!(id = %fixture.metadata.id, "Loaded fixture");
                Ok(Some(fixture))
            }
            Err(e) => {
                warn!(path = %path.display(), error = %e, "Failed to parse fixture");
                Ok(None)
            }
        }
    }

    /// Validate all fixtures in the corpus.
    #[instrument(skip(self))]
    pub fn validate(&self) -> Result<Vec<ValidationError>> {
        let mut errors = Vec::new();
        let fixtures = self.load_all(None)?;

        let mut seen_ids = std::collections::HashSet::new();

        for fixture in &fixtures {
            // Check for duplicate IDs
            if !seen_ids.insert(&fixture.metadata.id) {
                errors.push(ValidationError {
                    fixture_id: fixture.metadata.id.clone(),
                    message: "Duplicate fixture ID".to_string(),
                });
            }

            // Check for empty content
            if fixture.input.content.trim().is_empty() {
                errors.push(ValidationError {
                    fixture_id: fixture.metadata.id.clone(),
                    message: "Empty input content".to_string(),
                });
            }

            // Check for missing expectations
            if fixture.expected.must_contain.is_empty()
                && fixture.expected.must_not_contain.is_empty()
            {
                errors.push(ValidationError {
                    fixture_id: fixture.metadata.id.clone(),
                    message: "No expectations defined".to_string(),
                });
            }

            // Check for valid language
            let valid_languages = [
                "python",
                "rust",
                "go",
                "javascript",
                "typescript",
                "java",
                "yaml",
                "json",
                "toml",
                "ini",
                "env",
            ];
            if !valid_languages.contains(&fixture.metadata.language.as_str()) {
                errors.push(ValidationError {
                    fixture_id: fixture.metadata.id.clone(),
                    message: format!("Unknown language: {}", fixture.metadata.language),
                });
            }
        }

        Ok(errors)
    }

    /// List all fixture IDs with metadata.
    pub fn list(&self, category: Option<&str>) -> Result<Vec<FixtureSummary>> {
        let categories = category.map(|c| vec![c.to_string()]);
        let fixtures = self.load_all(categories.as_deref())?;

        Ok(fixtures
            .into_iter()
            .map(|f| FixtureSummary {
                id: f.metadata.id,
                name: f.metadata.name,
                category: f.metadata.category,
                language: f.metadata.language,
                must_contain_count: f.expected.must_contain.len(),
                must_not_contain_count: f.expected.must_not_contain.len(),
            })
            .collect())
    }
}

/// A validation error in a fixture.
#[derive(Debug, Clone)]
pub struct ValidationError {
    /// ID of the fixture with the error.
    pub fixture_id: String,
    /// Error message.
    pub message: String,
}

/// Summary of a fixture for listing.
#[derive(Debug, Clone)]
pub struct FixtureSummary {
    /// Fixture ID.
    pub id: String,
    /// Fixture name.
    pub name: String,
    /// Category.
    pub category: String,
    /// Language.
    pub language: String,
    /// Number of must_contain expectations.
    pub must_contain_count: usize,
    /// Number of must_not_contain expectations.
    pub must_not_contain_count: usize,
}

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

    fn create_test_fixture(id: &str, category: &str) -> String {
        format!(
            r#"
[metadata]
id = "{id}"
name = "Test fixture"
category = "{category}"
language = "python"

[input]
content = "verify=False"

[expected]
must_contain = [
    {{ subject = "tls/cert_verification", predicate = "enabled", value = false }}
]
"#
        )
    }

    #[test]
    fn test_parse_fixture() {
        let toml_content = create_test_fixture("test-001", "tls");
        let fixture: Fixture = toml::from_str(&toml_content).expect("parse fixture");

        assert_eq!(fixture.metadata.id, "test-001");
        assert_eq!(fixture.metadata.category, "tls");
        assert_eq!(fixture.expected.must_contain.len(), 1);
    }

    #[test]
    fn test_fixture_loader() {
        let temp_dir = TempDir::new().expect("temp dir");
        let tls_dir = temp_dir.path().join("tls");
        fs::create_dir(&tls_dir).expect("create tls dir");

        // Write manifest
        let manifest = r#"
[corpus]
version = "1.0.0"

[categories.tls]
fixtures = 1
description = "TLS fixtures"
"#;
        fs::write(temp_dir.path().join("manifest.toml"), manifest).expect("write manifest");

        // Write fixture
        let fixture = create_test_fixture("tls-001", "tls");
        fs::write(tls_dir.join("disabled_verification.toml"), fixture).expect("write fixture");

        let loader = FixtureLoader::new(temp_dir.path());
        let fixtures = loader.load_all(None).expect("load fixtures");

        assert_eq!(fixtures.len(), 1);
        assert_eq!(fixtures[0].metadata.id, "tls-001");
    }

    #[test]
    fn test_fixture_validation() {
        let temp_dir = TempDir::new().expect("temp dir");

        // Write manifest
        let manifest = r#"
[corpus]
version = "1.0.0"
"#;
        fs::write(temp_dir.path().join("manifest.toml"), manifest).expect("write manifest");

        // Write fixture with empty content
        let bad_fixture = r#"
[metadata]
id = "bad-001"
name = "Bad fixture"
category = "test"
language = "python"

[input]
content = ""

[expected]
"#;
        fs::write(temp_dir.path().join("bad.toml"), bad_fixture).expect("write fixture");

        let loader = FixtureLoader::new(temp_dir.path());
        let errors = loader.validate().expect("validate");

        assert!(!errors.is_empty());
        assert!(errors.iter().any(|e| e.message.contains("Empty input")));
    }

    #[test]
    fn test_expected_claim_with_rationale() {
        let toml_content = r#"
[metadata]
id = "test-001"
name = "Test fixture"
category = "tls"
language = "python"

[input]
content = "verify=False"

[expected]
must_contain = [
    { subject = "tls/cert_verification", predicate = "enabled", value = false, rationale = "verify=False disables TLS verification" }
]
"#;
        let fixture: Fixture = toml::from_str(toml_content).expect("parse fixture");
        let claim = &fixture.expected.must_contain[0];

        assert_eq!(claim.rationale.as_deref(), Some("verify=False disables TLS verification"));
    }
}
