//! OWASP Cheat Sheet corpus builder.
//!
//! This builder fetches OWASP Cheat Sheets from GitHub and extracts security
//! recommendations to create authoritative assertions.
//!
//! # Caching
//!
//! Cheat sheets are cached to `~/.cache/aphoria/owasp-cache/{filename}` to
//! minimize network requests.
//!
//! # Target Cheat Sheets
//!
//! | Filename                              | Topic           |
//! |---------------------------------------|-----------------|
//! | Authentication_Cheat_Sheet.md         | authentication  |
//! | JSON_Web_Token_for_Java_Cheat_Sheet.md | jwt            |
//! | Transport_Layer_Security_Cheat_Sheet.md | tls           |
//! | Secrets_Management_Cheat_Sheet.md     | secrets         |
//! | Input_Validation_Cheat_Sheet.md       | input_validation|
//! | Session_Management_Cheat_Sheet.md     | session         |

mod parsers;
#[cfg(test)]
mod tests;

use std::fs;
use std::thread;
use std::time::Duration;

use ed25519_dalek::SigningKey;
use stemedb_core::types::{Assertion, SourceClass};
use tracing::{debug, info, instrument, warn};

use super::CorpusBuilder;
use crate::config::CorpusConfig;
use crate::episteme::create_authoritative_assertion;
use crate::AphoriaError;
use parsers::parse_cheatsheet;

/// Target OWASP cheat sheets to fetch.
const TARGET_CHEAT_SHEETS: &[(&str, &str)] = &[
    ("Authentication_Cheat_Sheet.md", "authentication"),
    ("JSON_Web_Token_for_Java_Cheat_Sheet.md", "jwt"),
    ("Transport_Layer_Security_Cheat_Sheet.md", "tls"),
    ("Secrets_Management_Cheat_Sheet.md", "secrets"),
    ("Input_Validation_Cheat_Sheet.md", "input_validation"),
    ("Session_Management_Cheat_Sheet.md", "session"),
    ("Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.md", "csrf"),
    ("Password_Storage_Cheat_Sheet.md", "password_storage"),
    ("HTTP_Headers_Cheat_Sheet.md", "http_headers"),
];

/// Base URL for OWASP CheatSheetSeries raw content.
const OWASP_BASE_URL: &str =
    "https://raw.githubusercontent.com/OWASP/CheatSheetSeries/master/cheatsheets/";

/// HTTP timeout for fetching cheat sheets.
const FETCH_TIMEOUT_SECS: u64 = 30;

/// Rate limit delay between requests (milliseconds).
const RATE_LIMIT_MS: u64 = 500;

/// Builder for OWASP Cheat Sheet corpus.
pub struct OwaspCorpusBuilder {
    /// Cheat sheets to fetch.
    sheets: Vec<(String, String)>,
}

impl OwaspCorpusBuilder {
    /// Create a new OWASP corpus builder with default cheat sheets.
    pub fn new() -> Self {
        let sheets =
            TARGET_CHEAT_SHEETS.iter().map(|(f, t)| (f.to_string(), t.to_string())).collect();
        Self { sheets }
    }

    /// Create a builder with custom cheat sheets.
    #[allow(dead_code)]
    pub fn with_sheets(sheets: Vec<(String, String)>) -> Self {
        Self { sheets }
    }
}

impl Default for OwaspCorpusBuilder {
    fn default() -> Self {
        Self::new()
    }
}

impl CorpusBuilder for OwaspCorpusBuilder {
    fn name(&self) -> &str {
        "OWASP"
    }

    fn scheme(&self) -> &str {
        "owasp"
    }

    fn default_tier(&self) -> u8 {
        1 // Clinical
    }

    fn requires_network(&self) -> bool {
        true // Needs to fetch cheat sheets (unless cached)
    }

    fn source_ids(&self) -> Vec<String> {
        self.sheets.iter().map(|(_, topic)| format!("OWASP {}", topic)).collect()
    }

    #[instrument(skip(self, signing_key, config), fields(builder = "OWASP", sheets = self.sheets.len()))]
    fn build(
        &self,
        signing_key: &SigningKey,
        timestamp: u64,
        config: &CorpusConfig,
    ) -> Result<Vec<Assertion>, AphoriaError> {
        let cache_dir = config.cache_dir.join("owasp-cache");
        fs::create_dir_all(&cache_dir)?;

        let mut all_assertions = Vec::new();

        for (i, (filename, topic)) in self.sheets.iter().enumerate() {
            // Rate limiting between requests
            if i > 0 {
                thread::sleep(Duration::from_millis(RATE_LIMIT_MS));
            }

            match fetch_and_parse_cheatsheet(filename, topic, &cache_dir, signing_key, timestamp) {
                Ok(assertions) => {
                    info!(filename, topic, assertions = assertions.len(), "Parsed cheat sheet");
                    all_assertions.extend(assertions);
                }
                Err(e) => {
                    warn!(filename, topic, error = %e, "Failed to process cheat sheet");
                    // Continue with other sheets
                }
            }
        }

        Ok(all_assertions)
    }
}

/// Fetch a cheat sheet and parse security recommendations.
fn fetch_and_parse_cheatsheet(
    filename: &str,
    topic: &str,
    cache_dir: &std::path::Path,
    signing_key: &SigningKey,
    timestamp: u64,
) -> Result<Vec<Assertion>, AphoriaError> {
    let content = fetch_cheatsheet_content(filename, cache_dir)?;
    let recommendations = parse_cheatsheet(&content, topic);

    let assertions = recommendations
        .into_iter()
        .map(|rec| {
            create_authoritative_assertion(
                signing_key,
                &rec.subject,
                &rec.predicate,
                rec.value,
                SourceClass::Clinical, // Tier 1
                &rec.description,
                timestamp,
            )
        })
        .collect();

    Ok(assertions)
}

/// Fetch cheat sheet content, using cache if available.
fn fetch_cheatsheet_content(
    filename: &str,
    cache_dir: &std::path::Path,
) -> Result<String, AphoriaError> {
    let cache_path = cache_dir.join(filename);

    // Check cache first
    if cache_path.exists() {
        debug!(filename, "Loading from cache");
        return fs::read_to_string(&cache_path).map_err(|e| AphoriaError::OwaspFetch {
            sheet: filename.to_string(),
            message: e.to_string(),
        });
    }

    // Fetch from network
    let url = format!("{}{}", OWASP_BASE_URL, filename);
    info!(filename, url = %url, "Fetching cheat sheet");

    let response =
        ureq::get(&url).timeout(Duration::from_secs(FETCH_TIMEOUT_SECS)).call().map_err(|e| {
            AphoriaError::OwaspFetch { sheet: filename.to_string(), message: e.to_string() }
        })?;

    let content = response.into_string().map_err(|e| AphoriaError::OwaspFetch {
        sheet: filename.to_string(),
        message: e.to_string(),
    })?;

    // Cache the result
    if let Err(e) = fs::write(&cache_path, &content) {
        warn!(filename, error = %e, "Failed to cache cheat sheet");
    }

    Ok(content)
}
