//! Corpus building operations - fetching and ingesting authoritative sources.

use std::path::{Path, PathBuf};

use crate::bridge;
use crate::config::AphoriaConfig;
use crate::corpus::{CorpusBuildResult, CorpusBuilderInfo, CorpusRegistry};
use crate::current_timestamp;
use crate::episteme;
use crate::error::AphoriaError;
use crate::policy::TrustPack;
use stemedb_storage::KVStore;
use tracing::{info, instrument};

/// Arguments for corpus build command.
#[derive(Debug, Clone, Default)]
pub struct CorpusBuildArgs {
    /// Only include specific corpus sources (comma-separated: rfc,owasp,vendor).
    pub only: Option<Vec<String>>,
    /// Run in offline mode (skip sources requiring network).
    pub offline: bool,
    /// Clear cache before building.
    pub clear_cache: bool,
}

/// Build the authoritative corpus from configured sources.
///
/// This command:
/// 1. Fetches RFCs, OWASP cheat sheets, and vendor documentation
/// 2. Parses normative statements and recommendations
/// 3. Ingests them as assertions into the local Episteme instance
#[instrument(skip(config), fields(offline = args.offline, clear_cache = args.clear_cache))]
pub async fn build_corpus(
    args: CorpusBuildArgs,
    config: &AphoriaConfig,
) -> Result<CorpusBuildResult, AphoriaError> {
    info!("Building authoritative corpus");

    let project_root = std::env::current_dir()?;

    // Clear cache if requested
    if args.clear_cache {
        let cache_dir = &config.corpus.cache_dir;
        if cache_dir.exists() {
            info!(cache_dir = %cache_dir.display(), "Clearing corpus cache");
            std::fs::remove_dir_all(cache_dir)?;
        }
    }

    // Build corpus config based on --only flag
    let mut corpus_config = config.corpus.clone();
    if let Some(only) = &args.only {
        corpus_config.include_rfc = only.iter().any(|s| s == "rfc");
        corpus_config.include_owasp = only.iter().any(|s| s == "owasp");
        corpus_config.include_vendor = only.iter().any(|s| s == "vendor");
        corpus_config.use_community = only.iter().any(|s| s == "community");
    }

    // Open Episteme to get access to stores for community corpus
    let mut episteme = episteme::LocalEpisteme::open(config, &project_root).await?;

    // Open corpus database for CLI-created items (if configured)
    let corpus_store = if let Some(ref corpus_data_dir) = config.episteme.corpus_data_dir {
        let corpus_episteme =
            episteme::LocalEpisteme::open_corpus_db(corpus_data_dir, &project_root).await?;
        Some(corpus_episteme.store().clone())
    } else {
        None
    };

    // Create registry with stores (enables community corpus builder and CLI-created items)
    let kv_store = episteme.store().clone();
    let predicate_index =
        std::sync::Arc::new(stemedb_storage::GenericPredicateIndexStore::new(kv_store.clone()));
    let registry =
        CorpusRegistry::with_stores(&corpus_config, kv_store, predicate_index, corpus_store);

    // Load signing key
    let signing_key = bridge::load_or_generate_key(&project_root)?;

    // Build corpus
    let timestamp = current_timestamp();

    let result = registry.build_all(&signing_key, timestamp, &corpus_config, args.offline).await?;

    // Ingest into Episteme
    if !result.assertions.is_empty() {
        let ingested = episteme.ingest_authoritative(&result.assertions).await?;
        info!(ingested, "Corpus ingested into Episteme");
    }

    // Shutdown episteme
    episteme.shutdown().await;

    Ok(result)
}

/// List available corpus sources.
#[instrument(skip(config))]
pub fn list_corpus_sources(config: &AphoriaConfig) -> Vec<CorpusBuilderInfo> {
    info!("Listing corpus sources");

    let registry = CorpusRegistry::with_defaults(&config.corpus);
    registry.list_builders()
}

/// Export the corpus as a signed Trust Pack.
///
/// Builds the corpus from configured sources and packages it as a
/// distributable Trust Pack that can be imported into other projects.
#[instrument(skip(config), fields(name = %name, output = %output.display()))]
pub async fn export_corpus_as_pack(
    name: String,
    output: PathBuf,
    only: Option<Vec<String>>,
    offline: bool,
    config: &AphoriaConfig,
) -> Result<usize, AphoriaError> {
    info!("Exporting corpus as Trust Pack");

    let project_root = std::env::current_dir()?;

    // Build corpus config based on --only flag
    let mut corpus_config = config.corpus.clone();
    if let Some(only) = &only {
        corpus_config.include_rfc = only.iter().any(|s| s == "rfc");
        corpus_config.include_owasp = only.iter().any(|s| s == "owasp");
        corpus_config.include_vendor = only.iter().any(|s| s == "vendor");
    }

    // Create registry and build
    let registry = CorpusRegistry::with_defaults(&corpus_config);
    let signing_key = bridge::load_or_generate_key(&project_root)?;
    let timestamp = current_timestamp();

    let result = registry.build_all(&signing_key, timestamp, &corpus_config, offline).await?;

    if result.assertions.is_empty() {
        return Err(AphoriaError::Config("No assertions built — nothing to export".to_string()));
    }

    let assertion_count = result.assertions.len();

    // Include predicate aliases from config
    let predicate_aliases: Vec<crate::policy::PackPredicateAliasSet> = config
        .predicate_aliases
        .to_alias_sets()
        .iter()
        .map(crate::policy::PackPredicateAliasSet::from)
        .collect();

    // Package as Trust Pack
    let pack = TrustPack::new_with_predicate_aliases(
        name,
        "0.1.0".to_string(),
        result.assertions,
        vec![], // No aliases for corpus packs
        predicate_aliases,
        &signing_key,
        config.trust_pack.signer_name.clone(),
        config.trust_pack.contact.clone(),
    )?;

    pack.save(&output)?;

    info!(assertions = assertion_count, output = %output.display(), "Corpus exported as Trust Pack");
    Ok(assertion_count)
}

/// Import patterns from wiki documentation and store in corpus database.
///
/// This function:
/// 1. Parses wiki markdown to extract WikiPatterns
/// 2. Parses authority strings (RFC, OWASP, CWE) into structured Authority enums
/// 3. Builds proper subject URIs (rfc://, owasp://, cwe://, community://wiki/)
/// 4. Creates signed assertions with rich metadata
/// 5. Stores in corpus database (~/.aphoria/corpus-db/) NOT project database
///
/// # Arguments
///
/// * `wiki_path` - Path to directory containing markdown wiki files
/// * `config` - Aphoria configuration
///
/// # Returns
///
/// Number of patterns promoted to corpus database.
#[instrument(skip(config), fields(wiki_path = %wiki_path.as_ref().display()))]
pub async fn import_corpus_from_wiki<P: AsRef<Path>>(
    wiki_path: P,
    config: &AphoriaConfig,
) -> Result<usize, AphoriaError> {
    use crate::corpus::promote_wiki_patterns_to_corpus;
    use crate::corpus::WikiParser;

    info!("Importing wiki from: {}", wiki_path.as_ref().display());

    let project_root = std::env::current_dir()?;

    // Parse wiki files and extract WikiPatterns
    let parser = WikiParser::new()?;
    let mut patterns = Vec::new();

    let wiki_path = wiki_path.as_ref();
    if !wiki_path.exists() {
        return Err(AphoriaError::Config(format!(
            "Wiki path does not exist: {}",
            wiki_path.display()
        )));
    }

    // Walk directory for markdown files
    let walker = ignore::WalkBuilder::new(wiki_path).follow_links(true).build();

    for entry in walker.flatten() {
        if entry.file_type().is_some_and(|ft| ft.is_file()) {
            let path = entry.path();
            if let Some(ext) = path.extension() {
                if ext == "md" {
                    info!("Parsing wiki file: {}", path.display());
                    let content = tokio::fs::read_to_string(path).await?;
                    let file_patterns = parser.parse(&content)?;
                    patterns.extend(file_patterns);
                }
            }
        }
    }

    let pattern_count = patterns.len();

    if patterns.is_empty() {
        info!("No patterns found in wiki");
        return Ok(0);
    }

    info!(pattern_count, "Parsed {} patterns from wiki", pattern_count);

    // Get corpus_data_dir from config (required)
    let corpus_data_dir = config
        .episteme
        .corpus_data_dir
        .as_ref()
        .ok_or_else(|| AphoriaError::Config("corpus_data_dir not configured".into()))?;

    // Open corpus database (NOT project database)
    let mut corpus_episteme =
        episteme::LocalEpisteme::open_corpus_db(corpus_data_dir, &project_root).await?;

    // Get signing key from corpus episteme
    let signing_key = corpus_episteme.signing_key().clone();

    // Promote wiki patterns to corpus database
    let promoted =
        promote_wiki_patterns_to_corpus(patterns, &signing_key, corpus_episteme.get_kv_store())
            .await?;

    corpus_episteme.shutdown().await;

    info!(promoted, "Promoted {} wiki patterns to corpus database", promoted);
    Ok(promoted)
}

/// Create a single corpus item from structured fields.
///
/// This function is used by the `aphoria corpus create` CLI command and by
/// LLM-based extraction skills to programmatically add corpus items.
///
/// # Arguments
///
/// * `subject` - Hierarchical subject path (e.g., "ml/dependencies/basicsr/torchvision")
/// * `predicate` - Predicate name (e.g., "incompatible_with", "requires")
/// * `value` - Value as string (auto-detected as boolean, number, or text)
/// * `explanation` - Full context and explanation for this claim
/// * `authority` - Authority source (GitHub URL, paper citation, docs URL)
/// * `category` - Category (compatibility, performance, security, architecture)
/// * `tier` - Authority tier (0=regulatory, 1=clinical, 2=observational, 3=community)
/// * `config` - Aphoria configuration
///
/// # Returns
///
/// Corpus item ID in format "corpus://{subject}/{predicate}"
#[allow(clippy::too_many_arguments)]
#[instrument(skip(config), fields(subject = %subject, tier = tier))]
pub async fn create_corpus_item(
    subject: String,
    predicate: String,
    value: String,
    explanation: String,
    authority: String,
    category: String,
    tier: u8,
    config: &AphoriaConfig,
) -> Result<String, AphoriaError> {
    use crate::episteme::create_authoritative_assertion_with_metadata;
    use stemedb_core::types::SourceClass;

    // 1. Validate tier (0-3)
    let source_class = match tier {
        0 => SourceClass::Regulatory,
        1 => SourceClass::Clinical,
        2 => SourceClass::Observational,
        3 => SourceClass::Community,
        _ => return Err(AphoriaError::Config(format!("Invalid tier: {tier}. Must be 0-3"))),
    };

    // 2. Parse value into ObjectValue
    let object_value = parse_value_string(&value)?;

    // 3. Infer URI scheme if not present
    let subject_uri = infer_subject_uri(&subject, tier, &authority)?;

    // 4. Get project root and signing key
    let project_root = std::env::current_dir()?;
    let signing_key = bridge::load_or_generate_key(&project_root)?;

    // 5. Get corpus database path from config
    let corpus_data_dir = config
        .episteme
        .corpus_data_dir
        .as_ref()
        .ok_or_else(|| AphoriaError::Config("corpus_data_dir not configured".into()))?;

    // 6. Open corpus database
    let mut corpus_episteme =
        episteme::LocalEpisteme::open_corpus_db(corpus_data_dir, &project_root).await?;

    // 7. Build metadata
    let metadata = serde_json::json!({
        "description": explanation,
        "authority_source": authority,
        "category": category,
        "source": "cli_create"
    });

    // 8. Create signed assertion with URI-schemed subject
    let timestamp = current_timestamp();
    let assertion = create_authoritative_assertion_with_metadata(
        &signing_key,
        &subject_uri,
        &predicate,
        object_value,
        source_class,
        &explanation,
        timestamp,
        metadata,
    );

    // 9. Serialize and store
    let serialized = stemedb_core::serde::serialize(&assertion)
        .map_err(|e| AphoriaError::Storage(format!("Failed to serialize assertion: {e}")))?;

    // Store with subject index (use URI-schemed subject)
    let subject_key = format!("subject:{}", subject_uri);
    corpus_episteme
        .store()
        .put(subject_key.as_bytes(), &serialized)
        .await
        .map_err(|e| AphoriaError::Storage(format!("Failed to store: {e}")))?;

    // Store with predicate index
    let pred_key = format!("predicate:corpus:{}", predicate);
    corpus_episteme
        .store()
        .put(pred_key.as_bytes(), &serialized)
        .await
        .map_err(|e| AphoriaError::Storage(format!("Failed to store predicate index: {e}")))?;

    // 10. Shutdown and return
    corpus_episteme.shutdown().await;

    info!(subject = %subject_uri, predicate = %predicate, tier = tier, "Created corpus item");
    Ok(format!("corpus://{}/{}", subject_uri, predicate))
}

/// Infer URI scheme from authority and tier.
///
/// If the subject already has a scheme (contains "://"), return as-is.
/// Otherwise, infer scheme based on authority string and tier:
/// - RFC authority → rfc://
/// - OWASP authority → owasp://
/// - CWE authority → cwe://
/// - Tier 2 (observational) → vendor://
/// - Tier 3 (community) → community://
///
/// # Examples
///
/// ```
/// assert_eq!(infer_subject_uri("tls/validation", 0, "RFC 5280"), "rfc://tls/validation");
/// assert_eq!(infer_subject_uri("xss/prevention", 1, "OWASP Top 10"), "owasp://xss/prevention");
/// assert_eq!(infer_subject_uri("rfc://already/schemed", 0, "RFC 9999"), "rfc://already/schemed");
/// ```
fn infer_subject_uri(subject: &str, tier: u8, authority: &str) -> Result<String, AphoriaError> {
    // If already has scheme, return as-is
    if subject.contains("://") {
        return Ok(subject.to_string());
    }

    // Infer scheme from authority and tier (case-insensitive matching)
    let authority_lower = authority.to_lowercase();
    let scheme = if authority_lower.contains("rfc") {
        "rfc"
    } else if authority_lower.contains("owasp") {
        "owasp"
    } else if authority_lower.contains("cwe") {
        "cwe"
    } else if tier == 2 {
        "vendor"
    } else if tier == 3 {
        "community"
    } else {
        // For tier 0 or 1 without recognized authority, use "corpus" as fallback
        "corpus"
    };

    Ok(format!("{}://{}", scheme, subject))
}

/// Parse value string into ObjectValue.
///
/// Attempts to parse as boolean, then number, then defaults to text.
fn parse_value_string(value: &str) -> Result<stemedb_core::types::ObjectValue, AphoriaError> {
    use stemedb_core::types::ObjectValue;
    // Try boolean
    if value.eq_ignore_ascii_case("true") {
        return Ok(ObjectValue::Boolean(true));
    }
    if value.eq_ignore_ascii_case("false") {
        return Ok(ObjectValue::Boolean(false));
    }

    // Try number
    if let Ok(n) = value.parse::<f64>() {
        return Ok(ObjectValue::Number(n));
    }

    // Default to text
    Ok(ObjectValue::Text(value.to_string()))
}

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

    #[test]
    fn test_infer_subject_uri_rfc_authority() {
        // RFC authority should infer rfc:// scheme (case-insensitive)
        let result = infer_subject_uri("tls/validation", 0, "RFC 5280").unwrap();
        assert_eq!(result, "rfc://tls/validation");

        let result = infer_subject_uri("tls/cipher_suites", 1, "rfc 8446").unwrap();
        assert_eq!(result, "rfc://tls/cipher_suites");

        let result = infer_subject_uri("http/headers", 2, "Rfc 7231").unwrap();
        assert_eq!(result, "rfc://http/headers");
    }

    #[test]
    fn test_infer_subject_uri_owasp_authority() {
        // OWASP authority should infer owasp:// scheme (case-insensitive)
        let result = infer_subject_uri("xss/prevention", 0, "OWASP Top 10").unwrap();
        assert_eq!(result, "owasp://xss/prevention");

        let result = infer_subject_uri("csrf/token", 1, "owasp cheat sheet").unwrap();
        assert_eq!(result, "owasp://csrf/token");

        let result = infer_subject_uri("injection/sql", 2, "Owasp Guide").unwrap();
        assert_eq!(result, "owasp://injection/sql");
    }

    #[test]
    fn test_infer_subject_uri_cwe_authority() {
        // CWE authority should infer cwe:// scheme (case-insensitive)
        let result = infer_subject_uri("buffer/overflow", 0, "CWE-120").unwrap();
        assert_eq!(result, "cwe://buffer/overflow");

        let result = infer_subject_uri("path/traversal", 1, "cwe-22").unwrap();
        assert_eq!(result, "cwe://path/traversal");

        let result = infer_subject_uri("injection/command", 2, "Cwe-78").unwrap();
        assert_eq!(result, "cwe://injection/command");
    }

    #[test]
    fn test_infer_subject_uri_vendor_tier() {
        // Tier 2 (observational) should infer vendor:// scheme
        let result = infer_subject_uri("ml/dependencies", 2, "GitHub Issue #123").unwrap();
        assert_eq!(result, "vendor://ml/dependencies");

        let result = infer_subject_uri("api/rate_limit", 2, "Vendor Documentation").unwrap();
        assert_eq!(result, "vendor://api/rate_limit");
    }

    #[test]
    fn test_infer_subject_uri_community_tier() {
        // Tier 3 (community) should infer community:// scheme
        let result = infer_subject_uri("best_practices/logging", 3, "Team Wiki").unwrap();
        assert_eq!(result, "community://best_practices/logging");

        let result = infer_subject_uri("patterns/error_handling", 3, "Internal Docs").unwrap();
        assert_eq!(result, "community://patterns/error_handling");
    }

    #[test]
    fn test_infer_subject_uri_corpus_fallback() {
        // Tier 0 or 1 without recognized authority should use corpus:// fallback
        let result = infer_subject_uri("custom/subject", 0, "Unknown Authority").unwrap();
        assert_eq!(result, "corpus://custom/subject");

        let result = infer_subject_uri("another/subject", 1, "Some Other Source").unwrap();
        assert_eq!(result, "corpus://another/subject");
    }

    #[test]
    fn test_infer_subject_uri_already_schemed() {
        // Subjects with existing schemes should be returned as-is
        let result = infer_subject_uri("rfc://already/schemed", 0, "RFC 9999").unwrap();
        assert_eq!(result, "rfc://already/schemed");

        let result = infer_subject_uri("owasp://already/schemed", 1, "OWASP").unwrap();
        assert_eq!(result, "owasp://already/schemed");

        let result = infer_subject_uri("custom://some/path", 2, "Vendor").unwrap();
        assert_eq!(result, "custom://some/path");

        let result = infer_subject_uri("http://example.com/path", 3, "Community").unwrap();
        assert_eq!(result, "http://example.com/path");
    }

    #[test]
    fn test_infer_subject_uri_authority_priority() {
        // Authority string takes priority over tier for scheme inference
        let result = infer_subject_uri("test/subject", 3, "RFC 1234").unwrap();
        assert_eq!(result, "rfc://test/subject"); // RFC wins over tier 3

        let result = infer_subject_uri("test/subject", 2, "OWASP Guide").unwrap();
        assert_eq!(result, "owasp://test/subject"); // OWASP wins over tier 2

        let result = infer_subject_uri("test/subject", 3, "CWE-999").unwrap();
        assert_eq!(result, "cwe://test/subject"); // CWE wins over tier 3
    }

    #[test]
    fn test_parse_value_string_boolean() {
        use stemedb_core::types::ObjectValue;

        // Test boolean parsing (case-insensitive)
        assert_eq!(parse_value_string("true").unwrap(), ObjectValue::Boolean(true));
        assert_eq!(parse_value_string("TRUE").unwrap(), ObjectValue::Boolean(true));
        assert_eq!(parse_value_string("false").unwrap(), ObjectValue::Boolean(false));
        assert_eq!(parse_value_string("False").unwrap(), ObjectValue::Boolean(false));
    }

    #[test]
    fn test_parse_value_string_number() {
        use stemedb_core::types::ObjectValue;

        // Test number parsing
        assert_eq!(parse_value_string("42").unwrap(), ObjectValue::Number(42.0));
        assert_eq!(parse_value_string("3.14").unwrap(), ObjectValue::Number(3.14));
        assert_eq!(parse_value_string("-100").unwrap(), ObjectValue::Number(-100.0));
        assert_eq!(parse_value_string("0.0").unwrap(), ObjectValue::Number(0.0));
    }

    #[test]
    fn test_parse_value_string_text() {
        use stemedb_core::types::ObjectValue;

        // Test text parsing (fallback for non-boolean, non-number)
        assert_eq!(
            parse_value_string("hello world").unwrap(),
            ObjectValue::Text("hello world".to_string())
        );
        assert_eq!(
            parse_value_string("not_a_bool").unwrap(),
            ObjectValue::Text("not_a_bool".to_string())
        );
        assert_eq!(parse_value_string("1.2.3").unwrap(), ObjectValue::Text("1.2.3".to_string()));
    }
}
