//! Command handlers for authored claims management.

use std::process::ExitCode;

use aphoria::claims_explain;
use aphoria::claims_file::ClaimsFile;
use aphoria::pending_markers::{MarkerStatus, PendingMarkersFile};
use aphoria::AphoriaConfig;
use aphoria::{parse_authority_tier, AuthoredClaim, AuthoredValue, ClaimStatus};
use chrono::Utc;

use crate::cli::ClaimsCommands;

/// Find the project root by walking up from cwd looking for `.aphoria/claims.toml`.
///
/// Falls back to cwd if no claims file is found in any parent.
fn project_root() -> Result<std::path::PathBuf, ExitCode> {
    let cwd = std::env::current_dir().map_err(|e| {
        eprintln!("Error: cannot determine current directory: {e}");
        ExitCode::from(3)
    })?;

    // Check cwd first
    if cwd.join(".aphoria/claims.toml").exists() {
        return Ok(cwd);
    }

    // Walk up parents
    let mut dir = cwd.as_path();
    while let Some(parent) = dir.parent() {
        if parent.join(".aphoria/claims.toml").exists() {
            return Ok(parent.to_path_buf());
        }
        dir = parent;
    }

    // Fall back to cwd (will return empty claims)
    Ok(cwd)
}

/// Handle claims subcommands.
pub async fn handle_claims_command(command: ClaimsCommands, config: &AphoriaConfig) -> ExitCode {
    match command {
        ClaimsCommands::Create {
            id,
            concept_path,
            predicate,
            value,
            comparison,
            provenance,
            invariant,
            consequence,
            tier,
            evidence,
            category,
            by,
        } => {
            handle_claims_create(
                id,
                concept_path,
                predicate,
                value,
                comparison,
                provenance,
                invariant,
                consequence,
                tier,
                evidence,
                category,
                by,
                config,
            )
            .await
        }
        ClaimsCommands::List { category, status, format } => {
            handle_claims_list(category, status, format, config).await
        }
        ClaimsCommands::Explain { claim, output, format } => {
            handle_claims_explain(claim, output, format, config).await
        }
        ClaimsCommands::Update {
            id,
            provenance,
            invariant,
            consequence,
            tier,
            evidence,
            category,
            value,
        } => {
            handle_claims_update(
                id,
                provenance,
                invariant,
                consequence,
                tier,
                evidence,
                category,
                value,
                config,
            )
            .await
        }
        ClaimsCommands::Supersede {
            id,
            new_id,
            value,
            provenance,
            invariant,
            consequence,
            tier,
            evidence,
            by,
        } => {
            handle_claims_supersede(
                id,
                new_id,
                value,
                provenance,
                invariant,
                consequence,
                tier,
                evidence,
                by,
                config,
            )
            .await
        }
        ClaimsCommands::Deprecate { id, reason } => {
            handle_claims_deprecate(id, reason, config).await
        }
        ClaimsCommands::ListMarkers { status, format } => {
            handle_list_markers(status, format, config).await
        }
        ClaimsCommands::FormalizeMarker { marker_id, id, tier, evidence, by } => {
            handle_formalize_marker(marker_id, id, tier, evidence, by, config).await
        }
        ClaimsCommands::RejectMarker { marker_id, reason } => {
            handle_reject_marker(marker_id, reason, config).await
        }
        ClaimsCommands::Import {
            file,
            authority_tier,
            source_guide,
            dry_run,
            merge,
            template,
            validate_only,
            format,
        } => {
            let options = ImportOptions {
                file,
                authority_tier,
                source_guide,
                dry_run,
                merge,
                template,
                validate_only,
                format,
            };
            handle_claims_import(options, config).await
        }
    }
}

#[allow(clippy::too_many_arguments)]
async fn handle_claims_create(
    id: String,
    concept_path: String,
    predicate: String,
    value: String,
    comparison: String,
    provenance: String,
    invariant: String,
    consequence: String,
    tier: String,
    evidence: Vec<String>,
    category: String,
    by: String,
    _config: &AphoriaConfig,
) -> ExitCode {
    use aphoria::ComparisonMode;

    // Validate authority tier
    if let Err(e) = parse_authority_tier(&tier) {
        eprintln!("Error: {e}");
        return ExitCode::from(3);
    }

    // Parse comparison mode
    let comparison_mode = match comparison.to_lowercase().as_str() {
        "equals" => ComparisonMode::Equals,
        "not_equals" => ComparisonMode::NotEquals,
        "present" => ComparisonMode::Present,
        "absent" => ComparisonMode::Absent,
        "contains" => ComparisonMode::Contains,
        "not_contains" => ComparisonMode::NotContains,
        _ => {
            eprintln!(
                "Error: Invalid comparison mode '{comparison}'. Expected: equals, not_equals, present, absent, contains, not_contains"
            );
            return ExitCode::from(3);
        }
    };

    let root = match project_root() {
        Ok(r) => r,
        Err(code) => return code,
    };
    let path = ClaimsFile::default_path(&root);
    let mut claims_file = match ClaimsFile::load(&path) {
        Ok(f) => f,
        Err(e) => {
            eprintln!("Error loading claims file: {e}");
            return ExitCode::from(3);
        }
    };

    // Check for duplicate ID
    if claims_file.find_by_id(&id).is_some() {
        eprintln!("Error: Claim with ID '{id}' already exists");
        return ExitCode::from(3);
    }

    let now = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();

    let claim = AuthoredClaim {
        id: id.clone(),
        concept_path,
        predicate,
        value: AuthoredValue::parse(&value),
        comparison: comparison_mode,
        provenance,
        invariant,
        consequence,
        authority_tier: tier.to_lowercase(),
        evidence,
        category,
        status: ClaimStatus::Active,
        supersedes: None,
        created_by: by,
        created_at: now,
        updated_at: None,
    };

    claims_file.add(claim);

    if let Err(e) = claims_file.save(&path) {
        eprintln!("Error saving claims file: {e}");
        return ExitCode::from(3);
    }

    println!("Created claim '{id}' in {}", path.display());
    ExitCode::SUCCESS
}

async fn handle_claims_list(
    category: Option<String>,
    status: Option<String>,
    format: String,
    _config: &AphoriaConfig,
) -> ExitCode {
    let root = match project_root() {
        Ok(r) => r,
        Err(code) => return code,
    };
    let path = ClaimsFile::default_path(&root);
    let claims_file = match ClaimsFile::load(&path) {
        Ok(f) => f,
        Err(e) => {
            eprintln!("Error loading claims file: {e}");
            return ExitCode::from(3);
        }
    };

    let mut claims: Vec<&AuthoredClaim> = claims_file.claims.iter().collect();

    // Filter by category
    if let Some(ref cat) = category {
        claims.retain(|c| c.category == *cat);
    }

    // Filter by status
    if let Some(ref st) = status {
        let target = match st.to_lowercase().as_str() {
            "active" => ClaimStatus::Active,
            "deprecated" => ClaimStatus::Deprecated,
            "superseded" => ClaimStatus::Superseded,
            other => {
                eprintln!("Unknown status: {other}. Expected: active, deprecated, superseded");
                return ExitCode::from(3);
            }
        };
        claims.retain(|c| c.status == target);
    }

    if format == "json" {
        let envelope = serde_json::json!({
            "type": "claims_list",
            "total": claims.len(),
            "claims": claims
        });
        match serde_json::to_string_pretty(&envelope) {
            Ok(json) => println!("{json}"),
            Err(e) => {
                eprintln!("Error serializing claims: {e}");
                return ExitCode::from(3);
            }
        }
    } else {
        // Table format
        if claims.is_empty() {
            println!("No claims found.");
            return ExitCode::SUCCESS;
        }

        let mut table = comfy_table::Table::new();
        table.set_header(vec!["ID", "Category", "Tier", "Status", "Invariant"]);

        for claim in &claims {
            let invariant_short = if claim.invariant.len() > 50 {
                format!("{}...", &claim.invariant[..47])
            } else {
                claim.invariant.clone()
            };
            table.add_row(vec![
                &claim.id,
                &claim.category,
                &claim.authority_tier,
                &claim.status.to_string(),
                &invariant_short,
            ]);
        }

        println!("{table}");
        println!("\n{} claim(s) total", claims.len());
    }

    ExitCode::SUCCESS
}

async fn handle_claims_explain(
    claim_id: Option<String>,
    output: Option<std::path::PathBuf>,
    format: String,
    _config: &AphoriaConfig,
) -> ExitCode {
    let root = match project_root() {
        Ok(r) => r,
        Err(code) => return code,
    };
    let path = ClaimsFile::default_path(&root);
    let claims_file = match ClaimsFile::load(&path) {
        Ok(f) => f,
        Err(e) => {
            eprintln!("Error loading claims file: {e}");
            return ExitCode::from(3);
        }
    };

    let project_name = root
        .file_name()
        .map(|n| n.to_string_lossy().to_string())
        .unwrap_or_else(|| "project".to_string());

    let content = if let Some(ref id) = claim_id {
        // Single claim
        let claim = match claims_file.find_by_id(id) {
            Some(c) => c,
            None => {
                eprintln!("Claim not found: {id}");
                return ExitCode::from(3);
            }
        };
        if format == "json" {
            match claims_explain::render_claim_json(claim, &project_name) {
                Ok(json) => json,
                Err(e) => {
                    eprintln!("Error rendering claim: {e}");
                    return ExitCode::from(3);
                }
            }
        } else {
            let mut out = String::new();
            claims_explain::render_single_claim(&mut out, claim);
            out
        }
    } else {
        // All claims
        if format == "json" {
            match claims_explain::render_claims_json(&claims_file.claims, &project_name) {
                Ok(json) => json,
                Err(e) => {
                    eprintln!("Error rendering claims: {e}");
                    return ExitCode::from(3);
                }
            }
        } else {
            claims_explain::render_claims_markdown(&claims_file.claims, &project_name)
        }
    };

    if let Some(ref out_path) = output {
        if let Some(parent) = out_path.parent() {
            if !parent.exists() {
                if let Err(e) = std::fs::create_dir_all(parent) {
                    eprintln!("Error creating output directory: {e}");
                    return ExitCode::from(3);
                }
            }
        }
        if let Err(e) = std::fs::write(out_path, &content) {
            eprintln!("Error writing output: {e}");
            return ExitCode::from(3);
        }
        println!("Written to {}", out_path.display());
    } else {
        println!("{content}");
    }

    ExitCode::SUCCESS
}

#[allow(clippy::too_many_arguments)]
async fn handle_claims_update(
    id: String,
    provenance: Option<String>,
    invariant: Option<String>,
    consequence: Option<String>,
    tier: Option<String>,
    evidence: Vec<String>,
    category: Option<String>,
    value: Option<String>,
    _config: &AphoriaConfig,
) -> ExitCode {
    // Validate tier if provided
    if let Some(ref t) = tier {
        if let Err(e) = parse_authority_tier(t) {
            eprintln!("Error: {e}");
            return ExitCode::from(3);
        }
    }

    let root = match project_root() {
        Ok(r) => r,
        Err(code) => return code,
    };
    let path = ClaimsFile::default_path(&root);
    let mut claims_file = match ClaimsFile::load(&path) {
        Ok(f) => f,
        Err(e) => {
            eprintln!("Error loading claims file: {e}");
            return ExitCode::from(3);
        }
    };

    let now = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();

    let result = claims_file.update(&id, |c| {
        if let Some(p) = provenance {
            c.provenance = p;
        }
        if let Some(i) = invariant {
            c.invariant = i;
        }
        if let Some(con) = consequence {
            c.consequence = con;
        }
        if let Some(t) = tier {
            c.authority_tier = t.to_lowercase();
        }
        if !evidence.is_empty() {
            for e in evidence {
                if !c.evidence.contains(&e) {
                    c.evidence.push(e);
                }
            }
        }
        if let Some(cat) = category {
            c.category = cat;
        }
        if let Some(v) = value {
            c.value = AuthoredValue::parse(&v);
        }
        c.updated_at = Some(now);
    });

    if let Err(e) = result {
        eprintln!("Error: {e}");
        return ExitCode::from(3);
    }

    if let Err(e) = claims_file.save(&path) {
        eprintln!("Error saving claims file: {e}");
        return ExitCode::from(3);
    }

    println!("Updated claim '{id}'");
    ExitCode::SUCCESS
}

#[allow(clippy::too_many_arguments)]
async fn handle_claims_supersede(
    old_id: String,
    new_id: Option<String>,
    value: Option<String>,
    provenance: Option<String>,
    invariant: Option<String>,
    consequence: Option<String>,
    tier: Option<String>,
    evidence: Vec<String>,
    by: Option<String>,
    _config: &AphoriaConfig,
) -> ExitCode {
    let root = match project_root() {
        Ok(r) => r,
        Err(code) => return code,
    };
    let path = ClaimsFile::default_path(&root);
    let mut claims_file = match ClaimsFile::load(&path) {
        Ok(f) => f,
        Err(e) => {
            eprintln!("Error loading claims file: {e}");
            return ExitCode::from(3);
        }
    };

    // Get the old claim to copy fields from
    let old_claim = match claims_file.find_by_id(&old_id) {
        Some(c) => c.clone(),
        None => {
            eprintln!("Claim not found: {old_id}");
            return ExitCode::from(3);
        }
    };

    let now = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();
    let actual_new_id = new_id.unwrap_or_else(|| format!("{old_id}-v2"));

    // Check for duplicate
    if claims_file.find_by_id(&actual_new_id).is_some() {
        eprintln!("Error: Claim with ID '{actual_new_id}' already exists");
        return ExitCode::from(3);
    }

    // Validate new tier if provided
    let new_tier = tier.map(|t| t.to_lowercase()).unwrap_or(old_claim.authority_tier.clone());
    if let Err(e) = parse_authority_tier(&new_tier) {
        eprintln!("Error: {e}");
        return ExitCode::from(3);
    }

    let new_claim = AuthoredClaim {
        id: actual_new_id.clone(),
        concept_path: old_claim.concept_path.clone(),
        predicate: old_claim.predicate.clone(),
        value: value.map(|v| AuthoredValue::parse(&v)).unwrap_or(old_claim.value.clone()),
        comparison: old_claim.comparison.clone(),
        provenance: provenance.unwrap_or(old_claim.provenance.clone()),
        invariant: invariant.unwrap_or(old_claim.invariant.clone()),
        consequence: consequence.unwrap_or(old_claim.consequence.clone()),
        authority_tier: new_tier,
        evidence: if evidence.is_empty() { old_claim.evidence.clone() } else { evidence },
        category: old_claim.category.clone(),
        status: ClaimStatus::Active,
        supersedes: Some(old_id.clone()),
        created_by: by.unwrap_or(old_claim.created_by.clone()),
        created_at: now,
        updated_at: None,
    };

    if let Err(e) = claims_file.supersede(&old_id, new_claim) {
        eprintln!("Error: {e}");
        return ExitCode::from(3);
    }

    if let Err(e) = claims_file.save(&path) {
        eprintln!("Error saving claims file: {e}");
        return ExitCode::from(3);
    }

    println!("Created claim '{actual_new_id}' superseding '{old_id}'");
    ExitCode::SUCCESS
}

async fn handle_claims_deprecate(id: String, reason: String, _config: &AphoriaConfig) -> ExitCode {
    let root = match project_root() {
        Ok(r) => r,
        Err(code) => return code,
    };
    let path = ClaimsFile::default_path(&root);
    let mut claims_file = match ClaimsFile::load(&path) {
        Ok(f) => f,
        Err(e) => {
            eprintln!("Error loading claims file: {e}");
            return ExitCode::from(3);
        }
    };

    let now = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();

    // Update the claim with deprecation info
    let result = claims_file.update(&id, |c| {
        c.status = ClaimStatus::Deprecated;
        c.updated_at = Some(format!("{now} (deprecated: {reason})"));
    });

    if let Err(e) = result {
        eprintln!("Error: {e}");
        return ExitCode::from(3);
    }

    if let Err(e) = claims_file.save(&path) {
        eprintln!("Error saving claims file: {e}");
        return ExitCode::from(3);
    }

    println!("Deprecated claim '{id}': {reason}");
    ExitCode::SUCCESS
}

// ============================================================================
// Marker Management Handlers
// ============================================================================

async fn handle_list_markers(
    status_filter: Option<String>,
    format: String,
    _config: &AphoriaConfig,
) -> ExitCode {
    let root = match project_root() {
        Ok(r) => r,
        Err(code) => return code,
    };
    let path = PendingMarkersFile::default_path(&root);
    let markers_file = match PendingMarkersFile::load(&path) {
        Ok(f) => f,
        Err(e) => {
            eprintln!("Error loading pending markers: {e}");
            return ExitCode::from(3);
        }
    };

    // Filter markers by status if requested
    let markers = if let Some(status_str) = status_filter {
        let status = match status_str.to_lowercase().as_str() {
            "pending" => MarkerStatus::Pending,
            "formalized" => MarkerStatus::Formalized,
            "rejected" => MarkerStatus::Rejected,
            _ => {
                eprintln!(
                    "Error: Invalid status '{}'. Use: pending, formalized, or rejected",
                    status_str
                );
                return ExitCode::from(3);
            }
        };
        markers_file.filter_by_status(&status)
    } else {
        markers_file.markers.iter().collect()
    };

    if markers.is_empty() {
        println!("No pending markers found");
        return ExitCode::SUCCESS;
    }

    match format.as_str() {
        "table" => {
            // Table header
            println!(
                "{:<20} {:<40} {:>6} {:<12} {:<}",
                "ID", "File", "Line", "Category", "Invariant"
            );
            println!("{}", "=".repeat(120));
            for marker in markers {
                let category = marker.category.as_deref().unwrap_or("none");
                let invariant = if marker.invariant.len() > 50 {
                    format!("{}...", &marker.invariant[..47])
                } else {
                    marker.invariant.clone()
                };
                println!(
                    "{:<20} {:<40} {:>6} {:<12} {}",
                    marker.id, marker.file, marker.line, category, invariant
                );
            }
        }
        "json" => {
            let output = serde_json::json!({
                "type": "pending_markers",
                "total": markers.len(),
                "markers": markers,
            });
            match serde_json::to_string_pretty(&output) {
                Ok(json) => println!("{json}"),
                Err(e) => {
                    eprintln!("Error serializing JSON: {e}");
                    return ExitCode::from(3);
                }
            }
        }
        _ => {
            eprintln!("Error: Invalid format '{}'. Use: table or json", format);
            return ExitCode::from(3);
        }
    }

    ExitCode::SUCCESS
}

/// Validates a claim ID follows naming conventions.
fn validate_claim_id(id: &str) -> Result<(), String> {
    if id.is_empty() {
        return Err("Claim ID cannot be empty".to_string());
    }

    if id.len() > 64 {
        return Err("Claim ID too long (max 64 characters)".to_string());
    }

    // Must be kebab-case: alphanumeric + hyphens, no leading/trailing hyphens
    let mut prev_hyphen = false;
    let mut has_content = false;

    for (i, c) in id.chars().enumerate() {
        match c {
            'a'..='z' | '0'..='9' => {
                has_content = true;
                prev_hyphen = false;
            }
            '-' => {
                if i == 0 || i == id.len() - 1 || prev_hyphen {
                    return Err(format!(
                        "Claim ID must be kebab-case (no leading/trailing/consecutive hyphens): '{}'",
                        id
                    ));
                }
                prev_hyphen = true;
            }
            _ => {
                return Err(format!(
                    "Claim ID must be kebab-case (lowercase alphanumeric + hyphens): '{}'",
                    id
                ));
            }
        }
    }

    if !has_content {
        return Err("Claim ID must contain alphanumeric characters".to_string());
    }

    Ok(())
}

async fn handle_formalize_marker(
    marker_id: String,
    claim_id: String,
    tier: String,
    evidence: Vec<String>,
    by: String,
    _config: &AphoriaConfig,
) -> ExitCode {
    let root = match project_root() {
        Ok(r) => r,
        Err(code) => return code,
    };

    // Validate claim ID early
    if let Err(e) = validate_claim_id(&claim_id) {
        eprintln!("Error: Invalid claim ID: {}", e);
        eprintln!("Example: myapp-pool-max-001");
        return ExitCode::from(3);
    }

    // Check for ID collision
    let claims_path = ClaimsFile::default_path(&root);
    if let Ok(existing_claims) = ClaimsFile::load(&claims_path) {
        if existing_claims.find_by_id(&claim_id).is_some() {
            eprintln!("Error: Claim ID '{}' already exists", claim_id);
            eprintln!(
                "Use a different ID or update the existing claim with 'aphoria claims update'."
            );
            return ExitCode::from(3);
        }
    }

    // Load pending markers
    let markers_path = PendingMarkersFile::default_path(&root);
    let mut markers_file = match PendingMarkersFile::load(&markers_path) {
        Ok(f) => f,
        Err(e) => {
            eprintln!("Error loading pending markers: {e}");
            return ExitCode::from(3);
        }
    };

    // Find the marker
    let marker = match markers_file.find_by_id(&marker_id) {
        Some(m) => m.clone(),
        None => {
            eprintln!("Error: Marker '{}' not found", marker_id);
            return ExitCode::from(3);
        }
    };

    // Validate marker still exists in source file
    let file_path = root.join(&marker.file);
    let file_content = match std::fs::read_to_string(&file_path) {
        Ok(content) => content,
        Err(e) => {
            eprintln!("Error: Cannot read file '{}': {}", marker.file, e);
            eprintln!("The file may have been moved or deleted since marker was detected.");
            eprintln!("Run 'aphoria scan' to refresh markers.");
            return ExitCode::from(3);
        }
    };

    // Verify the marker is still at the expected line
    let lines: Vec<&str> = file_content.lines().collect();
    if marker.line == 0 || marker.line > lines.len() {
        eprintln!("Error: Marker line {} is out of bounds in '{}'", marker.line, marker.file);
        eprintln!("File may have been modified. Run 'aphoria scan' to refresh markers.");
        return ExitCode::from(3);
    }

    // Check if line still contains the marker pattern (basic check)
    let line_content = lines[marker.line - 1]; // Convert to 0-indexed
    if !line_content.contains("@aphoria:claim") && !line_content.contains("@aphoria:claimed") {
        eprintln!("Warning: Marker no longer exists at line {} in '{}'", marker.line, marker.file);
        eprintln!("Line content: {}", line_content);
        eprintln!("The file may have been modified. Proceeding anyway, but verify manually.");
        // Don't fail here - allow manual override, but warn
    }

    // Generate concept_path from marker file location
    // Extract file stem and build path: project/file_stem/line
    let file_stem = std::path::Path::new(&marker.file)
        .file_stem()
        .and_then(|s| s.to_str())
        .unwrap_or("unknown");

    let concept_path = format!(
        "{}/{}/{}",
        root.file_name().and_then(|n| n.to_str()).unwrap_or("project"),
        file_stem,
        marker.line
    );

    // Infer predicate and value from context
    // For now, use generic "inline_marker_value" predicate
    // TODO: In the skill, this should be smarter (analyze surrounding code)
    let predicate = "inline_marker_value".to_string();
    let value = AuthoredValue::Text(marker.invariant.clone());

    // Prompt for consequence if missing
    let consequence = marker.consequence.clone().unwrap_or_else(|| {
        eprintln!("Warning: Marker has no consequence. Consider adding one.");
        "Unspecified consequence".to_string()
    });

    // Create the claim
    let provenance = format!("Inline marker from {}:{}", marker.file, marker.line);
    let category = marker.category.clone().unwrap_or_else(|| "general".to_string());
    let now = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();

    let claim = AuthoredClaim {
        id: claim_id.clone(),
        concept_path,
        predicate,
        value,
        comparison: Default::default(),
        provenance,
        invariant: marker.invariant.clone(),
        consequence,
        authority_tier: tier,
        evidence,
        category,
        status: ClaimStatus::Active,
        supersedes: None,
        created_by: by,
        created_at: now,
        updated_at: None,
    };

    // Add to claims file
    let claims_path = ClaimsFile::default_path(&root);
    let mut claims_file = match ClaimsFile::load(&claims_path) {
        Ok(f) => f,
        Err(e) => {
            eprintln!("Error loading claims file: {e}");
            return ExitCode::from(3);
        }
    };

    claims_file.add(claim);

    if let Err(e) = claims_file.save(&claims_path) {
        eprintln!("Error saving claims file: {e}");
        return ExitCode::from(3);
    }

    // Update marker status
    if let Err(e) = markers_file.update_status(
        &marker_id,
        MarkerStatus::Formalized,
        Some(claim_id.clone()),
        None,
    ) {
        eprintln!("Error updating marker status: {e}");
        return ExitCode::from(3);
    }

    if let Err(e) = markers_file.save(&markers_path) {
        eprintln!("Error saving markers file: {e}");
        return ExitCode::from(3);
    }

    println!("✓ Marker {} formalized to claim {}", marker_id, claim_id);
    println!("  File: {}:{}", marker.file, marker.line);
    println!();
    println!("Consider updating the comment:");
    let display_category = marker.category.as_deref().unwrap_or("category");
    println!("  // @aphoria:claim[{}] {}", display_category, marker.invariant);
    println!("  →");
    println!("  // @aphoria:claimed {}", claim_id);

    ExitCode::SUCCESS
}

async fn handle_reject_marker(
    marker_id: String,
    reason: String,
    _config: &AphoriaConfig,
) -> ExitCode {
    let root = match project_root() {
        Ok(r) => r,
        Err(code) => return code,
    };

    let markers_path = PendingMarkersFile::default_path(&root);
    let mut markers_file = match PendingMarkersFile::load(&markers_path) {
        Ok(f) => f,
        Err(e) => {
            eprintln!("Error loading pending markers: {e}");
            return ExitCode::from(3);
        }
    };

    if let Err(e) =
        markers_file.update_status(&marker_id, MarkerStatus::Rejected, None, Some(reason.clone()))
    {
        eprintln!("Error: {e}");
        return ExitCode::from(3);
    }

    if let Err(e) = markers_file.save(&markers_path) {
        eprintln!("Error saving markers file: {e}");
        return ExitCode::from(3);
    }

    println!("✗ Marker {} rejected: \"{}\"", marker_id, reason);
    ExitCode::SUCCESS
}

/// Options for bulk claim import.
struct ImportOptions {
    file: Option<std::path::PathBuf>,
    authority_tier: Option<String>,
    source_guide: Option<String>,
    dry_run: bool,
    merge: String,
    template: bool,
    validate_only: bool,
    format: String,
}

/// Validation error for a specific claim in the import file.
#[derive(Debug)]
struct ValidationError {
    claim_index: usize,
    claim_id: Option<String>,
    field: String,
    error: String,
}

/// Warning for a claim that's valid but potentially problematic.
#[derive(Debug)]
struct ValidationWarning {
    #[allow(dead_code)]
    claim_index: usize,
    claim_id: String,
    message: String,
}

/// Result of validating all claims in an import file.
#[derive(Debug)]
struct ValidationResult {
    errors: Vec<ValidationError>,
    warnings: Vec<ValidationWarning>,
}

impl ValidationResult {
    fn is_valid(&self) -> bool {
        self.errors.is_empty()
    }
}

/// Action taken for a specific claim during import.
#[derive(Debug, serde::Serialize)]
#[serde(rename_all = "lowercase")]
enum ImportAction {
    Added,
    Skipped,
    Overwritten,
}

/// Detail for a single claim in the import report.
#[derive(Debug, serde::Serialize)]
struct ImportDetail {
    action: ImportAction,
    claim_id: String,
    reason: Option<String>,
}

/// Summary counts for the import operation.
#[derive(Debug, serde::Serialize)]
struct ImportSummary {
    total_in_file: usize,
    added: usize,
    skipped: usize,
    overwritten: usize,
    failed: usize,
}

/// Complete import report with summary, details, and warnings.
#[derive(Debug, serde::Serialize)]
struct ImportReport {
    summary: ImportSummary,
    details: Vec<ImportDetail>,
    warnings: Vec<String>,
}

impl ImportReport {
    /// Format as human-readable table.
    fn format_table(&self, dry_run: bool) -> String {
        let mut output = String::new();

        output.push_str("Aphoria Claims Import\n");
        output.push_str(&"=".repeat(40));
        output.push('\n');

        if dry_run {
            output.push_str("🔍 Dry-run mode (no changes written)\n\n");
        } else {
            output.push_str("✓ Import Complete\n\n");
        }

        output.push_str("Summary:\n");
        output.push_str(&format!("  Total claims in file:  {}\n", self.summary.total_in_file));
        output.push_str(&format!("  Added:                 {}\n", self.summary.added));
        output.push_str(&format!("  Skipped:               {}\n", self.summary.skipped));
        output.push_str(&format!("  Overwritten:           {}\n", self.summary.overwritten));

        if !self.details.is_empty() {
            output.push_str("\nDetails:\n");
            for detail in &self.details {
                let symbol = match detail.action {
                    ImportAction::Added => "✓",
                    ImportAction::Skipped => "⊗",
                    ImportAction::Overwritten => "↻",
                };
                let action_str = match detail.action {
                    ImportAction::Added => "ADD   ",
                    ImportAction::Skipped => "SKIP  ",
                    ImportAction::Overwritten => "UPDATE",
                };
                let reason_str = detail.reason.as_ref()
                    .map(|r| format!(" ({})", r))
                    .unwrap_or_default();
                output.push_str(&format!("  {} {}  {}{}\n", symbol, action_str, detail.claim_id, reason_str));
            }
        }

        if !self.warnings.is_empty() {
            output.push_str("\nWarnings:\n");
            for warning in &self.warnings {
                output.push_str(&format!("  ⚠  {}\n", warning));
            }
        }

        output
    }

    /// Format as JSON for tooling integration.
    fn format_json(&self) -> Result<String, serde_json::Error> {
        serde_json::to_string_pretty(self)
    }
}

/// Generates an example TOML template for bulk claim import.
fn generate_import_template() -> String {
    r#"# Aphoria Claims Import Template
#
# This file contains example claims that demonstrate the TOML format for bulk import.
# To import claims:
#   aphoria claims import my-claims.toml
#
# To preview without writing:
#   aphoria claims import my-claims.toml --dry-run
#
# To validate format:
#   aphoria claims import my-claims.toml --validate-only
#
# Merge strategies:
#   --merge skip_existing    (default) Skip claims with duplicate IDs
#   --merge overwrite        Replace existing claims with same ID
#   --merge fail_on_duplicate  Exit with error if any ID exists

# Example 1: Architecture Decision
[[claim]]
id = "myapp-core-no-tokio-001"
concept_path = "myapp/core/imports/tokio"
predicate = "imported"
value = false
comparison = "absent"
provenance = "Architecture decision by tech lead 2024-12-15"
invariant = "Core modules MUST remain sync-only"
consequence = "Importing tokio makes core async-only, breaking sync library users"
authority_tier = "expert"
evidence = ["ADR-003", "design review notes"]
category = "architecture"
status = "active"
created_by = "tech-lead"
created_at = "2024-12-15T10:00:00Z"

# Example 2: Security Requirement
[[claim]]
id = "myapp-http-tls-cert-validation-001"
concept_path = "myapp/httpclient/tls/certificate_validation"
predicate = "enabled"
value = true
comparison = "equals"
provenance = "OWASP A02:2021 - Cryptographic Failures"
invariant = "HTTP clients MUST validate TLS certificates"
consequence = "Disabled validation exposes MITM attacks"
authority_tier = "regulatory"
evidence = ["OWASP Top 10", "CWE-295"]
category = "security"
status = "active"
created_by = "security-team"
created_at = "2024-12-15T10:00:00Z"

# Example 3: Safety Invariant
[[claim]]
id = "myapp-pool-max-size-001"
concept_path = "myapp/pool/config/max_size"
predicate = "max_value"
value = 50
comparison = "equals"
provenance = "Load testing results 2024-12-10"
invariant = "Connection pool size MUST NOT exceed 50"
consequence = "Larger pools cause OOM under sustained load"
authority_tier = "expert"
evidence = ["tests/pool_tests.rs load test"]
category = "safety"
status = "active"
created_by = "sre-team"
created_at = "2024-12-15T10:00:00Z"

# Field Reference:
#
# Required fields:
#   id              - Unique kebab-case identifier (e.g., "myapp-feature-001")
#   concept_path    - Hierarchical path to concept (e.g., "myapp/module/feature")
#   predicate       - Property being claimed (e.g., "enabled", "max_version")
#   value           - Expected value (bool, number, or "text")
#   comparison      - How to compare: equals, not_equals, present, absent, contains, not_contains
#   provenance      - Where this rule came from
#   invariant       - What MUST remain true
#   consequence     - What breaks if violated
#   authority_tier  - regulatory, clinical, observational, expert, community, anecdotal
#   category        - safety, architecture, security, imports, constants, derives, etc.
#   created_by      - Author name
#   created_at      - ISO 8601 timestamp
#
# Optional fields:
#   evidence        - Array of supporting references (default: [])
#   status          - active (default), deprecated, superseded
#   supersedes      - ID of claim this replaces
#   updated_at      - ISO 8601 timestamp of last update
"#.to_string()
}

/// Validates all claims before import.
///
/// Checks:
/// - ID format (kebab-case, length)
/// - Authority tier validity
/// - Required fields presence
/// - Duplicate IDs within import file
/// - Duplicate concept_path+predicate combinations (warning)
fn validate_imported_claims(
    claims: &[aphoria::AuthoredClaim],
    existing_claims: &ClaimsFile,
) -> ValidationResult {
    use std::collections::{HashMap, HashSet};

    let mut errors = Vec::new();
    let mut warnings = Vec::new();
    let mut seen_ids = HashSet::new();
    let mut seen_concepts = HashMap::new();

    for (index, claim) in claims.iter().enumerate() {
        // Validate ID format
        if let Err(e) = validate_claim_id(&claim.id) {
            errors.push(ValidationError {
                claim_index: index,
                claim_id: Some(claim.id.clone()),
                field: "id".to_string(),
                error: e,
            });
        }

        // Check for duplicate IDs within import file
        if seen_ids.contains(&claim.id) {
            errors.push(ValidationError {
                claim_index: index,
                claim_id: Some(claim.id.clone()),
                field: "id".to_string(),
                error: format!("Duplicate ID '{}' within import file", claim.id),
            });
        }
        seen_ids.insert(claim.id.clone());

        // Validate authority tier
        if let Err(e) = parse_authority_tier(&claim.authority_tier) {
            errors.push(ValidationError {
                claim_index: index,
                claim_id: Some(claim.id.clone()),
                field: "authority_tier".to_string(),
                error: e.to_string(),
            });
        }

        // Check required fields
        if claim.provenance.trim().is_empty() {
            errors.push(ValidationError {
                claim_index: index,
                claim_id: Some(claim.id.clone()),
                field: "provenance".to_string(),
                error: "provenance cannot be empty".to_string(),
            });
        }

        if claim.invariant.trim().is_empty() {
            errors.push(ValidationError {
                claim_index: index,
                claim_id: Some(claim.id.clone()),
                field: "invariant".to_string(),
                error: "invariant cannot be empty".to_string(),
            });
        }

        if claim.consequence.trim().is_empty() {
            errors.push(ValidationError {
                claim_index: index,
                claim_id: Some(claim.id.clone()),
                field: "consequence".to_string(),
                error: "consequence cannot be empty".to_string(),
            });
        }

        if claim.category.trim().is_empty() {
            errors.push(ValidationError {
                claim_index: index,
                claim_id: Some(claim.id.clone()),
                field: "category".to_string(),
                error: "category cannot be empty".to_string(),
            });
        }

        if claim.created_by.trim().is_empty() {
            errors.push(ValidationError {
                claim_index: index,
                claim_id: Some(claim.id.clone()),
                field: "created_by".to_string(),
                error: "created_by cannot be empty".to_string(),
            });
        }

        // Warn on duplicate concept_path + predicate (not an error, but suspicious)
        let concept_key = format!("{}::{}", claim.concept_path, claim.predicate);
        if let Some(&prev_index) = seen_concepts.get(&concept_key) {
            warnings.push(ValidationWarning {
                claim_index: index,
                claim_id: claim.id.clone(),
                message: format!(
                    "Duplicate concept_path+predicate '{}' (also in claim at index {})",
                    concept_key, prev_index
                ),
            });
        } else {
            seen_concepts.insert(concept_key, index);
        }

        // Warn if ID already exists in existing claims file
        if existing_claims.find_by_id(&claim.id).is_some() {
            warnings.push(ValidationWarning {
                claim_index: index,
                claim_id: claim.id.clone(),
                message: format!("Claim ID '{}' already exists in claims file", claim.id),
            });
        }
    }

    ValidationResult { errors, warnings }
}

async fn handle_claims_import(options: ImportOptions, _config: &AphoriaConfig) -> ExitCode {
    use aphoria::claims_file::ClaimsFile;
    use aphoria::AuthoredClaim;

    // Destructure options
    let ImportOptions {
        file,
        authority_tier,
        source_guide,
        dry_run,
        merge,
        template,
        validate_only,
        format,
    } = options;

    // Handle --template flag
    if template {
        print!("{}", generate_import_template());
        return ExitCode::SUCCESS;
    }

    // Require file path if not generating template
    let file = match file {
        Some(f) => f,
        None => {
            eprintln!("Error: FILE path required (or use --template to generate example)");
            return ExitCode::from(3);
        }
    };

    // Get project root
    let root = match project_root() {
        Ok(r) => r,
        Err(code) => return code,
    };

    // Load import file
    let import_content = match std::fs::read_to_string(&file) {
        Ok(c) => c,
        Err(e) => {
            eprintln!("Error reading import file: {e}");
            return ExitCode::from(3);
        }
    };

    // Parse claims from TOML
    #[derive(serde::Deserialize)]
    struct ImportFile {
        claim: Vec<AuthoredClaim>,
    }

    let mut import: ImportFile = match toml::from_str(&import_content) {
        Ok(i) => i,
        Err(e) => {
            eprintln!("Error parsing import file: {e}");
            return ExitCode::from(3);
        }
    };

    // Override authority tier if specified
    if let Some(ref tier) = authority_tier {
        // Validate the override tier first
        if let Err(e) = parse_authority_tier(tier) {
            eprintln!("Error: Invalid authority tier '{}': {}", tier, e);
            return ExitCode::from(3);
        }
        for claim in &mut import.claim {
            claim.authority_tier = tier.clone();
        }
    }

    // Load existing claims
    let claims_path = ClaimsFile::default_path(&root);
    let mut claims_file = match ClaimsFile::load(&claims_path) {
        Ok(f) => f,
        Err(e) => {
            eprintln!("Error loading claims file: {e}");
            return ExitCode::from(3);
        }
    };

    // Validate all claims before any writes
    let validation = validate_imported_claims(&import.claim, &claims_file);

    // Report validation errors
    if !validation.is_valid() {
        eprintln!("\n❌ Validation Failed\n");
        eprintln!("Found {} error(s) in import file:\n", validation.errors.len());
        for err in &validation.errors {
            let claim_desc = err
                .claim_id
                .as_ref()
                .map(|id| format!("'{}'", id))
                .unwrap_or_else(|| format!("at index {}", err.claim_index));
            eprintln!("  • Claim {} - {}: {}", claim_desc, err.field, err.error);
        }
        eprintln!("\nFix these errors and try again.");
        return ExitCode::from(3);
    }

    // Report validation warnings
    if !validation.warnings.is_empty() {
        eprintln!("\n⚠️  {} warning(s):\n", validation.warnings.len());
        for warn in &validation.warnings {
            eprintln!("  • Claim '{}': {}", warn.claim_id, warn.message);
        }
        eprintln!();
    }

    // If validate-only mode, report success and exit
    if validate_only {
        println!("\n✓ Validation passed");
        println!("  Total claims: {}", import.claim.len());
        println!("  Warnings: {}", validation.warnings.len());
        println!("\nFile is ready for import.");
        return ExitCode::SUCCESS;
    }

    // Process claims and build report
    let total_in_file = import.claim.len();
    let mut added_count = 0;
    let mut skipped_count = 0;
    let mut overwritten_count = 0;
    let mut details = Vec::new();
    let mut report_warnings = Vec::new();

    // Collect validation warnings for report
    for warn in &validation.warnings {
        report_warnings.push(format!("{}: {}", warn.claim_id, warn.message));
    }

    for claim in import.claim {
        let existing = claims_file.claims.iter().position(|c| c.id == claim.id);

        match (existing, merge.as_str()) {
            (Some(_idx), "skip_existing") => {
                skipped_count += 1;
                details.push(ImportDetail {
                    action: ImportAction::Skipped,
                    claim_id: claim.id.clone(),
                    reason: Some("already exists".to_string()),
                });
            }
            (Some(idx), "overwrite") => {
                if !dry_run {
                    claims_file.claims[idx] = claim.clone();
                }
                overwritten_count += 1;
                details.push(ImportDetail {
                    action: ImportAction::Overwritten,
                    claim_id: claim.id.clone(),
                    reason: None,
                });
            }
            (Some(_), "fail_on_duplicate") => {
                eprintln!("Error: Duplicate claim ID: {}", claim.id);
                return ExitCode::from(3);
            }
            (None, _) => {
                if !dry_run {
                    claims_file.claims.push(claim.clone());
                }
                added_count += 1;
                details.push(ImportDetail {
                    action: ImportAction::Added,
                    claim_id: claim.id.clone(),
                    reason: None,
                });
            }
            _ => {
                eprintln!("Invalid merge strategy: {merge}");
                return ExitCode::from(3);
            }
        }
    }

    // Save (unless dry-run)
    if !dry_run {
        if let Err(e) = claims_file.save(&claims_path) {
            eprintln!("Error saving claims file: {e}");
            return ExitCode::from(3);
        }

        // Track guideline metadata if source_guide is provided
        if let Some(guide_name) = source_guide {
            use aphoria::ingested_guides::{GuidelineMetadata, IngestedGuidesFile};

            let guides_path = IngestedGuidesFile::default_path(&root);
            let mut guides_file = IngestedGuidesFile::load(&guides_path).unwrap_or_default();

            // Compute document hash if source file exists
            let (source_path, document_hash) = if let Ok(content) = std::fs::read(&file) {
                use blake3::Hasher;
                let mut hasher = Hasher::new();
                hasher.update(&content);
                let hash = hasher.finalize();
                (Some(file.clone()), Some(format!("blake3:{}", hash.to_hex())))
            } else {
                (None, None)
            };

            // Create guideline metadata
            let guideline = GuidelineMetadata {
                id: guide_name.clone(),
                name: guide_name.clone(),
                source_path,
                document_hash,
                ingested_at: Utc::now().to_rfc3339(),
                claims_count: added_count + overwritten_count,
                authority_tier: authority_tier.clone().unwrap_or_else(|| "team_policy".to_string()),
                category: "imported".to_string(),
                claim_ids: claims_file
                    .claims
                    .iter()
                    .rev()
                    .take(added_count + overwritten_count)
                    .map(|c| c.id.clone())
                    .collect(),
            };

            guides_file.upsert(guideline);

            if let Err(e) = guides_file.save(&guides_path) {
                eprintln!("Warning: Failed to save guideline tracking: {e}");
            } else {
                println!("  Guideline tracked: {guide_name}");
            }
        }
    }

    // Build report
    let report = ImportReport {
        summary: ImportSummary {
            total_in_file,
            added: added_count,
            skipped: skipped_count,
            overwritten: overwritten_count,
            failed: 0,
        },
        details,
        warnings: report_warnings,
    };

    // Output report in requested format
    match format.as_str() {
        "json" => {
            match report.format_json() {
                Ok(json) => println!("{}", json),
                Err(e) => {
                    eprintln!("Error formatting JSON output: {}", e);
                    return ExitCode::from(3);
                }
            }
        }
        "table" => {
            println!("{}", report.format_table(dry_run));
        }
        _ => {
            eprintln!("Error: Invalid format '{}'. Use: table or json", format);
            return ExitCode::from(3);
        }
    }

    ExitCode::SUCCESS
}
