//! Policy export and import operations.

use std::path::PathBuf;

use tracing::{info, instrument, warn};

use crate::bridge;
use crate::config::AphoriaConfig;
use crate::episteme::LocalEpisteme;
use crate::error::AphoriaError;
use crate::policy::{PackPredicateAliasSet, SignatureRecord, TrustPack};
use crate::types::{predicates, AcknowledgeArgs, ExtractedClaim, UpdateArgs};

/// Export policy from the current project.
///
/// Collects all acknowledged conflicts, blessed patterns, and manual aliases into a Trust Pack.
#[instrument(skip(config))]
pub async fn export_policy(
    name: String,
    output: PathBuf,
    config: &AphoriaConfig,
) -> Result<(), AphoriaError> {
    info!(name, output = %output.display(), "Exporting policy");

    let project_root = std::env::current_dir()?;
    let episteme = LocalEpisteme::open(config, &project_root).await?;

    // Fetch acknowledgments (assertions with predicate="acknowledged")
    let mut assertions = episteme.fetch_acknowledgments().await?;
    let ack_count = assertions.len();

    // Fetch blessed assertions (patterns blessed as authoritative standards)
    let blessed = episteme.fetch_blessed_assertions().await?;
    let blessed_count = blessed.len();
    assertions.extend(blessed);

    info!(
        acknowledged = ack_count,
        blessed = blessed_count,
        total = assertions.len(),
        "Collected assertions for export"
    );

    // Fetch manual aliases
    let aliases = episteme.fetch_manual_aliases().await?;

    // Sign with agent key
    let signing_key = bridge::load_or_generate_key(&project_root)?;

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

    info!(
        predicate_alias_sets = predicate_aliases.len(),
        "Including predicate aliases from config"
    );

    let pack = TrustPack::new_with_predicate_aliases(
        name,
        "0.1.0".to_string(),
        assertions,
        aliases,
        predicate_aliases,
        &signing_key,
        config.trust_pack.signer_name.clone(),
        config.trust_pack.contact.clone(),
    )?;

    pack.save(&output)?;

    info!("Policy exported to {}", output.display());
    Ok(())
}

/// Statistics returned from policy import.
#[derive(Debug, Clone, Default)]
pub struct ImportStats {
    /// Number of assertions imported.
    pub assertions_imported: usize,
    /// Number of aliases imported.
    pub aliases_imported: usize,
    /// Number of predicate alias sets imported.
    pub predicate_aliases_imported: usize,
}

/// Statistics returned from policy re-signing.
#[derive(Debug, Clone, Default)]
pub struct ResignStats {
    /// Number of assertions in the pack.
    pub assertions_count: usize,
    /// Number of aliases in the pack.
    pub aliases_count: usize,
    /// Length of the signature chain (audit trail).
    pub signature_chain_length: usize,
}

/// Import a Trust Pack into the local Episteme.
///
/// Loads and verifies the pack, then imports assertions and aliases
/// into the local storage.
#[instrument(skip(config), fields(file = %file.display()))]
pub async fn import_policy(
    file: PathBuf,
    config: &AphoriaConfig,
) -> Result<ImportStats, AphoriaError> {
    use stemedb_storage::{AliasStore, PackSourceInfo, PackSourceStore, PredicateIndexStore};

    info!(file = %file.display(), "Importing policy");

    // Load and verify the pack
    let pack = TrustPack::load(&file)?;

    info!(
        name = %pack.header.name,
        version = %pack.header.version,
        assertions = pack.assertions.len(),
        aliases = pack.aliases.len(),
        "Trust pack verified"
    );

    // Open local Episteme
    let project_root = std::env::current_dir()?;
    let mut episteme = LocalEpisteme::open(config, &project_root).await?;

    let mut stats = ImportStats::default();

    // Import assertions via ingest_authoritative
    if !pack.assertions.is_empty() {
        let ingested = episteme.ingest_authoritative(&pack.assertions).await?;
        stats.assertions_imported = ingested;

        // Build pack source info for attribution
        let pack_info = PackSourceInfo {
            pack_name: pack.header.name.clone(),
            pack_version: pack.header.version.clone(),
            issuer_hex: hex::encode(&pack.header.issuer_id[..4]),
            signer_name: pack.header.signer_name.clone(),
            contact: pack.header.contact.clone(),
        };

        // Update predicate indexes and store pack source for all assertions
        // This is needed because ingest_authoritative goes through the WAL,
        // which doesn't update these indexes directly
        let predicate_store =
            stemedb_storage::GenericPredicateIndexStore::new(episteme.store().clone());

        for assertion in &pack.assertions {
            // Compute hash same way as ingestion
            let bytes = stemedb_core::serde::serialize(assertion).map_err(|e| {
                AphoriaError::Storage(format!(
                    "Failed to serialize assertion for {}: {e}",
                    assertion.subject
                ))
            })?;
            let hash = *blake3::hash(&bytes).as_bytes();

            // Store pack source for policy attribution
            if let Err(e) =
                episteme.pack_source_store().set_pack_source(&assertion.subject, &pack_info).await
            {
                warn!(
                    subject = %assertion.subject,
                    error = %e,
                    "Failed to store pack source"
                );
            }

            // Add ALL imported assertions to "authoritative" index for conflict detection
            if let Err(e) =
                predicate_store.add_to_predicate_index(predicates::AUTHORITATIVE, &hash).await
            {
                warn!(
                    subject = %assertion.subject,
                    error = %e,
                    "Failed to add to authoritative index"
                );
            }

            // Also add to "acknowledged" index if applicable
            if assertion.predicate == predicates::ACKNOWLEDGED {
                if let Err(e) =
                    predicate_store.add_to_predicate_index(predicates::ACKNOWLEDGED, &hash).await
                {
                    warn!(
                        subject = %assertion.subject,
                        error = %e,
                        "Failed to add to acknowledged index"
                    );
                }
            }
        }
    }

    // Import aliases
    for alias in &pack.aliases {
        let alias_store = stemedb_storage::GenericAliasStore::new(episteme.store().clone());
        alias_store.set_alias(alias).await.map_err(|e| {
            AphoriaError::Storage(format!(
                "Failed to import alias '{}' -> '{}': {e}",
                alias.alias, alias.canonical
            ))
        })?;
        stats.aliases_imported += 1;
    }

    // Persist predicate aliases to storage AND update in-memory cache
    // This ensures aliases survive restarts (Phase 6.5.3)
    if !pack.predicate_aliases.is_empty() {
        let alias_sets: Vec<crate::types::PredicateAliasSet> =
            pack.predicate_aliases.iter().map(crate::types::PredicateAliasSet::from).collect();

        episteme.persist_predicate_aliases(alias_sets).await?;

        info!(count = pack.predicate_aliases.len(), "Imported and persisted predicate alias sets");
        stats.predicate_aliases_imported = pack.predicate_aliases.len();
    }

    episteme.shutdown().await;

    info!(
        assertions = stats.assertions_imported,
        aliases = stats.aliases_imported,
        "Policy imported successfully"
    );
    Ok(stats)
}

/// Acknowledge a conflict as intentional.
///
/// Creates an assertion in Episteme recording that this conflict has been
/// reviewed and accepted. The conflict still appears in reports but marked as ACK.
///
/// If `args.expires` is provided, the acknowledgment will expire at that time.
/// Expired acknowledgments are preserved for audit trail (per patent claim 25)
/// but the conflict will resurface as BLOCK/FLAG.
#[instrument(skip(config), fields(concept_path = %args.concept_path))]
pub async fn acknowledge(
    args: AcknowledgeArgs,
    config: &AphoriaConfig,
) -> Result<(), AphoriaError> {
    use crate::expiry;

    info!("Acknowledging conflict");

    // Parse expiry if provided
    let expires_at =
        if let Some(ref spec) = args.expires { Some(expiry::parse_expiry(spec)?) } else { None };

    let project_root = std::env::current_dir()?;
    let mut episteme = LocalEpisteme::open(config, &project_root).await?;

    // Build acknowledgment payload as JSON
    // This allows storing both reason and expiry while maintaining backwards compatibility
    // (legacy acks stored as plain text are still readable)
    let ack_payload = serde_json::json!({
        "reason": args.reason,
        "expires_at": expires_at,
    });

    // Create acknowledgment assertion with JSON payload
    let claim = ExtractedClaim {
        concept_path: args.concept_path.clone(),
        predicate: predicates::ACKNOWLEDGED.to_string(),
        value: stemedb_core::types::ObjectValue::Text(ack_payload.to_string()),
        file: "aphoria_ack".to_string(),
        line: 0,
        matched_text: format!("Acknowledged: {}", args.reason),
        confidence: 1.0,
        description: format!("Conflict acknowledged: {}", args.reason),
    };

    episteme.ingest_claims(&[claim]).await?;
    episteme.shutdown().await;

    // Log expiry info if set
    if let Some(ts) = expires_at {
        info!(
            concept_path = %args.concept_path,
            expires = %expiry::format_expiry(ts),
            "Acknowledgment created with expiry"
        );
    }

    Ok(())
}

/// Arguments for the bless command.
pub use crate::types::BlessArgs;

/// Bless a code pattern as the authoritative standard.
///
/// Unlike `acknowledge` (which creates a suppression with predicate="acknowledged"),
/// `bless` creates an assertion with the actual predicate and value that becomes
/// the authoritative standard. Blessed patterns can be exported as Trust Packs
/// and imported into other projects.
///
/// # Example
///
/// ```ignore
/// // Bless TLS as required
/// bless(BlessArgs {
///     concept_path: "code://rust/grpc/tls".to_string(),
///     predicate: "enabled".to_string(),
///     value: "true".to_string(),
///     reason: "All services MUST use mTLS".to_string(),
/// }, &config).await?;
/// ```
#[instrument(skip(config), fields(concept_path = %args.concept_path, predicate = %args.predicate))]
pub async fn bless(args: BlessArgs, config: &AphoriaConfig) -> Result<(), AphoriaError> {
    info!("Blessing code pattern as standard");

    let project_root = std::env::current_dir()?;
    let mut episteme = LocalEpisteme::open(config, &project_root).await?;

    // Parse the value string into ObjectValue
    let value = parse_value(&args.value);

    // Create the blessed assertion with the actual predicate (not "acknowledged")
    let claim = ExtractedClaim {
        concept_path: args.concept_path.clone(),
        predicate: args.predicate.clone(), // The actual predicate, not "acknowledged"
        value,
        file: "aphoria_bless".to_string(),
        line: 0,
        matched_text: format!("Blessed: {} = {}", args.predicate, args.value),
        confidence: 1.0,
        description: args.reason.clone(),
    };

    episteme.ingest_claims(&[claim]).await?;
    episteme.shutdown().await;

    info!(concept_path = %args.concept_path, predicate = %args.predicate, "Pattern blessed as standard");
    Ok(())
}

/// Record an intentional policy change.
///
/// Unlike `acknowledge()` which marks a conflict as reviewed,
/// `update()` records a new baseline value for a concept. Use this
/// when you intentionally change a configuration and want future
/// scans to recognize this as the expected value.
///
/// # Example
///
/// ```ignore
/// // Record intentional change to pool size
/// update(UpdateArgs {
///     concept_path: "db/pool_size".to_string(),
///     value: "100".to_string(),
///     reason: "Scaling for Black Friday".to_string(),
/// }, &config).await?;
/// ```
#[instrument(skip(config), fields(concept_path = %args.concept_path))]
pub async fn update(args: UpdateArgs, config: &AphoriaConfig) -> Result<(), AphoriaError> {
    info!("Recording policy update");

    let project_root = std::env::current_dir()?;
    let mut episteme = LocalEpisteme::open(config, &project_root).await?;

    // Parse the value string into ObjectValue
    let value = parse_value(&args.value);

    // Create policy update assertion
    let claim = ExtractedClaim {
        concept_path: args.concept_path.clone(),
        predicate: predicates::POLICY_UPDATE.to_string(),
        value,
        file: "aphoria_update".to_string(),
        line: 0,
        matched_text: format!("Policy update: {} = {}", args.concept_path, args.value),
        confidence: 1.0,
        description: format!("Intentional change: {}", args.reason),
    };

    episteme.ingest_claims(&[claim]).await?;
    episteme.shutdown().await;

    info!(
        concept_path = %args.concept_path,
        value = %args.value,
        "Policy update recorded"
    );
    Ok(())
}

/// Re-sign a Trust Pack with a new key.
///
/// Loads an existing pack (without verifying the old signature), re-signs with
/// a new key, and optionally preserves the signature chain for audit trail.
///
/// # Arguments
///
/// * `file` - Path to the existing .pack file
/// * `output` - Path for the re-signed pack
/// * `key_path` - Optional path to the new signing key (defaults to .aphoria/agent.key)
/// * `reason` - Optional reason for re-signing (e.g., "Key rotation", "Security incident")
/// * `chain_signatures` - Whether to preserve the signature chain for audit trail
///
/// # Example
///
/// ```ignore
/// // Re-sign with new key, preserving audit trail
/// resign_policy(
///     "old.pack".into(),
///     "new.pack".into(),
///     None,  // Use default key
///     Some("Annual key rotation".to_string()),
///     true,  // Preserve chain
/// ).await?;
/// ```
#[instrument(skip_all, fields(file = %file.display(), output = %output.display()))]
pub async fn resign_policy(
    file: PathBuf,
    output: PathBuf,
    key_path: Option<PathBuf>,
    reason: Option<String>,
    chain_signatures: bool,
) -> Result<ResignStats, AphoriaError> {
    // 1. Load existing pack (skip verification - key may have changed)
    let pack = TrustPack::load_unverified(&file)?;
    info!(
        name = %pack.header.name,
        version = %pack.header.version,
        assertions = pack.assertions.len(),
        "Loaded pack for re-signing"
    );

    // 2. Load new signing key
    let project_root = std::env::current_dir()?;
    let key_file = key_path.unwrap_or_else(|| project_root.join(".aphoria/agent.key"));
    let signing_key = bridge::load_key_from_file(&key_file)?;

    // 3. Build signature chain (audit trail)
    let mut chain = pack.signature_chain.clone();
    if chain_signatures {
        chain.push(SignatureRecord {
            issuer_id: pack.header.issuer_id,
            signature: pack.signature,
            signed_at: pack.header.timestamp,
            reason,
        });

        info!(chain_length = chain.len(), "Preserving signature chain for audit");
    }

    // 4. Create new pack with updated signature
    // Preserve signer info from original pack
    let new_pack = TrustPack::resign(
        pack.header.name.clone(),
        pack.header.version.clone(),
        pack.assertions.clone(),
        pack.aliases.clone(),
        pack.predicate_aliases.clone(),
        &signing_key,
        chain.clone(),
        pack.header.signer_name.clone(),
        pack.header.contact.clone(),
    )?;

    // 5. Save to output
    new_pack.save(&output)?;

    info!(
        output = %output.display(),
        "Re-signed pack saved"
    );

    Ok(ResignStats {
        assertions_count: new_pack.assertions.len(),
        aliases_count: new_pack.aliases.len(),
        signature_chain_length: new_pack.signature_chain.len(),
    })
}

/// Parse a string value into an ObjectValue.
///
/// Supports:
/// - "true"/"false" → Boolean
/// - Finite numeric strings → Number (rejects NaN/Infinity)
/// - Everything else → Text
pub fn parse_value(s: &str) -> stemedb_core::types::ObjectValue {
    use stemedb_core::types::ObjectValue;

    match s.to_lowercase().as_str() {
        "true" => ObjectValue::Boolean(true),
        "false" => ObjectValue::Boolean(false),
        _ => {
            // Try to parse as number, but reject NaN and Infinity
            // as they're likely unintended and could cause issues downstream
            if let Ok(n) = s.parse::<f64>() {
                if n.is_finite() {
                    ObjectValue::Number(n)
                } else {
                    ObjectValue::Text(s.to_string())
                }
            } else {
                ObjectValue::Text(s.to_string())
            }
        }
    }
}

// ============================================================================
// Ack Export/Import (Version Control)
// ============================================================================

/// Statistics from ack export.
#[derive(Debug, Clone, Default)]
pub struct AckExportStats {
    /// Number of acks exported.
    pub exported: usize,
    /// Path to the export file.
    pub output_path: std::path::PathBuf,
}

/// Statistics from ack import.
#[derive(Debug, Clone, Default)]
pub struct AckImportStats {
    /// Number of acks imported (created new).
    pub imported: usize,
    /// Number of acks skipped (already exist).
    pub skipped: usize,
}

/// Export acknowledgments to a version-controllable TOML file.
///
/// Reads all acks from the local Episteme and writes them to `.aphoria/acks.toml`.
/// This file can be committed to version control for team collaboration.
#[instrument(skip(config))]
pub async fn export_acks(
    output: Option<std::path::PathBuf>,
    config: &AphoriaConfig,
) -> Result<AckExportStats, AphoriaError> {
    use crate::ack_file::{AckEntry, AckFile, AckPayload};

    info!("Exporting acknowledgments to file");

    let project_root = std::env::current_dir()?;
    let episteme = LocalEpisteme::open(config, &project_root).await?;

    // Fetch all acks from database
    let acks = episteme.fetch_acknowledgments().await?;

    // Convert to AckFile format
    let mut ack_file = AckFile::new();

    for assertion in &acks {
        // Parse the JSON payload to extract reason and expiry
        let payload_text = match &assertion.object {
            stemedb_core::types::ObjectValue::Text(t) => t.clone(),
            other => format!("{other:?}"),
        };
        let payload = AckPayload::parse(&payload_text);

        // Format created timestamp (timestamp is u64, need to convert for chrono)
        let created = i64::try_from(assertion.timestamp)
            .ok()
            .and_then(|ts| chrono::DateTime::from_timestamp(ts, 0))
            .map(|dt| dt.format("%Y-%m-%dT%H:%M:%SZ").to_string())
            .unwrap_or_else(|| format!("{}", assertion.timestamp));

        let expires = payload.expires_iso();
        ack_file.add(AckEntry {
            path: assertion.subject.clone(),
            reason: payload.reason,
            expires,
            created,
            by: None, // Could add agent_id here if we want to track who created it
        });
    }

    // Determine output path
    let output_path = output.unwrap_or_else(|| AckFile::default_path(&project_root));

    // Save to file
    ack_file.save(&output_path)?;

    info!(
        count = ack_file.len(),
        path = %output_path.display(),
        "Acknowledgments exported"
    );

    Ok(AckExportStats { exported: ack_file.len(), output_path })
}

/// Import acknowledgments from a TOML file into the local Episteme.
///
/// Reads `.aphoria/acks.toml` and creates acknowledgment assertions for each entry.
/// Skips entries that already exist (based on concept path).
#[instrument(skip(config))]
pub async fn import_acks(
    input: Option<std::path::PathBuf>,
    config: &AphoriaConfig,
) -> Result<AckImportStats, AphoriaError> {
    use crate::ack_file::AckFile;
    use crate::expiry;

    info!("Importing acknowledgments from file");

    let project_root = std::env::current_dir()?;
    let mut episteme = LocalEpisteme::open(config, &project_root).await?;

    // Determine input path
    let input_path = input.unwrap_or_else(|| AckFile::default_path(&project_root));

    if !input_path.exists() {
        return Err(AphoriaError::Config(format!("Ack file not found: {}", input_path.display())));
    }

    // Load ack file
    let ack_file = AckFile::load(&input_path)?;

    info!(count = ack_file.len(), path = %input_path.display(), "Loaded ack file");

    // Fetch existing acks to avoid duplicates
    let existing_acks = episteme.fetch_acknowledgments().await?;
    let existing_paths: std::collections::HashSet<&str> =
        existing_acks.iter().map(|a| a.subject.as_str()).collect();

    let mut stats = AckImportStats::default();

    for entry in &ack_file.acks {
        // Check if ack already exists
        if existing_paths.contains(entry.path.as_str()) {
            stats.skipped += 1;
            continue;
        }

        // Parse expiry if present
        let expires_at: Option<i64> = if let Some(ref exp_str) = entry.expires {
            // Try to parse as ISO 8601 timestamp
            chrono::DateTime::parse_from_rfc3339(exp_str).map(|dt| dt.timestamp()).ok().or_else(
                || {
                    // Fall back to expiry::parse_expiry for duration strings
                    // parse_expiry returns u64, convert to i64
                    expiry::parse_expiry(exp_str).ok().and_then(|u| i64::try_from(u).ok())
                },
            )
        } else {
            None
        };

        // Build ack payload as JSON
        let ack_payload = serde_json::json!({
            "reason": entry.reason,
            "expires_at": expires_at,
        });

        // Create acknowledgment assertion
        let claim = ExtractedClaim {
            concept_path: entry.path.clone(),
            predicate: predicates::ACKNOWLEDGED.to_string(),
            value: stemedb_core::types::ObjectValue::Text(ack_payload.to_string()),
            file: "aphoria_ack_import".to_string(),
            line: 0,
            matched_text: format!("Acknowledged: {}", entry.reason),
            confidence: 1.0,
            description: format!("Imported: {}", entry.reason),
        };

        episteme.ingest_claims(&[claim]).await?;
        stats.imported += 1;
    }

    episteme.shutdown().await;

    info!(imported = stats.imported, skipped = stats.skipped, "Acknowledgments imported");

    Ok(stats)
}
