//! StemeDB-backed claim storage handlers.
//!
//! These endpoints provide claim storage DIRECTLY in StemeDB (not `.aphoria/claims.toml`).
//! Used for remote/hosted mode where claims are stored in the knowledge graph.

use axum::{
    extract::{Path, State},
    http::StatusCode,
    Json,
};
use ed25519_dalek::{Signer, SigningKey, VerifyingKey};
use tracing::info;

use stemedb_core::signing::compute_content_hash_v2;
use stemedb_core::types::{Assertion, LifecycleStage, ObjectValue, SignatureEntry};
use stemedb_ingest::worker::serialize_assertion;
use stemedb_storage::{key_codec, KVStore};

use crate::{
    dto::{
        AuthoredClaimDto, AuthoredValueDto, ClaimSearchQuery, ClaimStatsDto, CreateClaimRequest,
        CreateClaimResponse,
    },
    error::{ApiError, Result},
    AppState,
};

// ============================================================================
// Server Signing Key Generation

/// Generate a deterministic server signing key from a seed.
/// This ensures the same key is used across server restarts.
fn get_server_signing_key() -> SigningKey {
    // Use a fixed seed for deterministic key generation
    // In production, this should be loaded from a config file or environment
    let seed = [0x42u8; 32]; // Placeholder seed - replace with actual key management
    SigningKey::from_bytes(&seed)
}

/// Sign an assertion with the server's key and add the signature.
fn add_server_signature(assertion: &mut Assertion) {
    let signing_key = get_server_signing_key();
    let verifying_key: VerifyingKey = (&signing_key).into();

    // Compute content hash for v2 signature
    let content_hash = compute_content_hash_v2(assertion);

    // Sign the content hash
    let signature_bytes = signing_key.sign(&content_hash);

    // Create signature entry
    let sig_entry = SignatureEntry {
        agent_id: verifying_key.to_bytes(),
        signature: signature_bytes.to_bytes(),
        timestamp: assertion.timestamp,
        version: 2, // Use v2 (enterprise) signing
    };

    assertion.signatures.push(sig_entry);
}

// ============================================================================
// Handlers
// ============================================================================

/// Create or update a claim in StemeDB.
#[utoipa::path(
    post,
    path = "/v1/claims",
    request_body = CreateClaimRequest,
    responses(
        (status = 201, description = "Claim created successfully", body = CreateClaimResponse),
        (status = 400, description = "Invalid request"),
        (status = 500, description = "Internal server error")
    ),
    tag = "claims"
)]
pub async fn create_claim(
    State(state): State<AppState>,
    Json(req): Json<CreateClaimRequest>,
) -> Result<(StatusCode, Json<CreateClaimResponse>)> {
    info!(claim_id = %req.claim.id, concept_path = %req.claim.concept_path, "Creating claim in StemeDB");

    // Convert DTO to Assertion
    let mut assertion = dto_to_assertion(&req.claim)?;

    // Add server signature to the assertion
    add_server_signature(&mut assertion);

    // Serialize and append to WAL
    let payload = serialize_assertion(&assertion)
        .map_err(|e| ApiError::Serialization(format!("Failed to serialize assertion: {e}")))?;

    state.commit_buffer.append(payload).await?;

    Ok((StatusCode::CREATED, Json(CreateClaimResponse { id: req.claim.id.clone(), stored: true })))
}

/// List all claims, optionally filtered.
#[utoipa::path(
    get,
    path = "/v1/claims",
    params(
        ("concept_path" = Option<String>, Query, description = "Filter by concept path"),
        ("predicate" = Option<String>, Query, description = "Filter by predicate"),
        ("authority_tier" = Option<String>, Query, description = "Filter by authority tier"),
    ),
    responses(
        (status = 200, description = "Claims retrieved successfully", body = Vec<AuthoredClaimDto>),
        (status = 500, description = "Internal server error")
    ),
    tag = "claims"
)]
pub async fn list_claims(
    State(state): State<AppState>,
    axum::extract::Query(filters): axum::extract::Query<std::collections::HashMap<String, String>>,
) -> Result<Json<Vec<AuthoredClaimDto>>> {
    info!("Listing claims from StemeDB");

    // Scan for all subjects starting with "claim://"
    let subjects_prefix = b"\x00SUBJECTS:claim://";
    let subject_entries = state.store.scan_prefix(subjects_prefix).await?;

    // For each subject, fetch all assertions by scanning the assertion prefix directly.
    // Assertions are stored as {subject}\x00H:{hash_hex} -> serialized bytes.
    let mut claims = Vec::new();
    for (key, _) in subject_entries {
        if let Some(subject) = key_codec::extract_subject_from_subjects_key(&key) {
            // Scan all assertion entries for this subject: {subject}\x00H:*
            let assertion_scan_prefix = key_codec::assertion_prefix(&subject);
            let assertion_entries = state.store.scan_prefix(&assertion_scan_prefix).await?;

            for (_, data) in assertion_entries {
                if let Ok(assertion) = stemedb_core::serde::deserialize_assertion_compat(&data) {
                    if let Ok(dto) = assertion_to_dto(&assertion) {
                        claims.push(dto);
                    }
                }
            }
        }
    }

    // Apply filters
    if let Some(concept_path) = filters.get("concept_path") {
        claims.retain(|c| c.concept_path == *concept_path);
    }
    if let Some(predicate) = filters.get("predicate") {
        claims.retain(|c| c.predicate == *predicate);
    }
    if let Some(authority_tier) = filters.get("authority_tier") {
        claims.retain(|c| c.authority_tier == *authority_tier);
    }

    Ok(Json(claims))
}

/// Get a specific claim by concept path and predicate.
#[utoipa::path(
    get,
    path = "/v1/claims/{concept_path}/{predicate}",
    params(
        ("concept_path" = String, Path, description = "Concept path"),
        ("predicate" = String, Path, description = "Predicate"),
    ),
    responses(
        (status = 200, description = "Claim found", body = AuthoredClaimDto),
        (status = 404, description = "Claim not found"),
        (status = 500, description = "Internal server error")
    ),
    tag = "claims"
)]
pub async fn get_claim(
    State(state): State<AppState>,
    Path((concept_path, predicate)): Path<(String, String)>,
) -> Result<Json<AuthoredClaimDto>> {
    info!(concept_path, predicate, "Getting claim from StemeDB");

    let subject = format!("claim://{}/{}", concept_path, predicate);

    // Scan assertion entries directly: {subject}\x00H:* -> serialized assertion bytes
    let assertion_scan_prefix = key_codec::assertion_prefix(&subject);
    let assertion_entries = state.store.scan_prefix(&assertion_scan_prefix).await?;

    if assertion_entries.is_empty() {
        return Err(ApiError::NotFound(format!("Claim not found: {}/{}", concept_path, predicate)));
    }

    let (_, data) = &assertion_entries[0];

    let assertion = stemedb_core::serde::deserialize_assertion_compat(data)
        .map_err(|e| ApiError::Serialization(format!("Failed to deserialize assertion: {e}")))?;

    assertion_to_dto(&assertion)
        .map(Json)
        .map_err(|e| ApiError::Internal(format!("Failed to convert assertion to DTO: {e}")))
}

/// Delete a claim (marks as deprecated).
#[utoipa::path(
    delete,
    path = "/v1/claims/{concept_path}/{predicate}",
    params(
        ("concept_path" = String, Path, description = "Concept path"),
        ("predicate" = String, Path, description = "Predicate"),
    ),
    responses(
        (status = 200, description = "Claim deprecated successfully"),
        (status = 404, description = "Claim not found"),
        (status = 500, description = "Internal server error")
    ),
    tag = "claims"
)]
pub async fn delete_claim(
    State(state): State<AppState>,
    Path((concept_path, predicate)): Path<(String, String)>,
) -> Result<StatusCode> {
    info!(concept_path, predicate, "Deleting claim from StemeDB");

    // Load the claim first
    let subject = format!("claim://{}/{}", concept_path, predicate);

    // Scan assertion entries directly: {subject}\x00H:* -> serialized assertion bytes
    let assertion_scan_prefix = key_codec::assertion_prefix(&subject);
    let assertion_entries = state.store.scan_prefix(&assertion_scan_prefix).await?;

    if assertion_entries.is_empty() {
        return Err(ApiError::NotFound(format!("Claim not found: {}/{}", concept_path, predicate)));
    }

    let (_, data) = &assertion_entries[0];

    let mut assertion = stemedb_core::serde::deserialize_assertion_compat(data)
        .map_err(|e| ApiError::Serialization(format!("Failed to deserialize assertion: {e}")))?;

    // Mark as deprecated (append-only: create new version)
    assertion.lifecycle = LifecycleStage::Deprecated;

    // Serialize and append to WAL
    let payload = serialize_assertion(&assertion)
        .map_err(|e| ApiError::Serialization(format!("Failed to serialize assertion: {e}")))?;

    state.commit_buffer.append(payload).await?;

    Ok(StatusCode::OK)
}

// ============================================================================
// Conversion Helpers
// ============================================================================

fn dto_to_assertion(dto: &AuthoredClaimDto) -> Result<Assertion> {
    // Parse object value
    let object = match &dto.value {
        AuthoredValueDto::Bool(b) => ObjectValue::Boolean(*b),
        AuthoredValueDto::Number(n) => ObjectValue::Number(*n),
        AuthoredValueDto::Text(s) => ObjectValue::Text(s.clone()),
    };

    // Parse source class from authority tier
    let source_class = parse_authority_tier(&dto.authority_tier)?;

    // Parse lifecycle from status
    let lifecycle = match dto.status.to_lowercase().as_str() {
        "draft" => LifecycleStage::Proposed,
        "active" => LifecycleStage::Approved,
        "deprecated" => LifecycleStage::Deprecated,
        "superseded" => LifecycleStage::Deprecated,
        _ => return Err(ApiError::InvalidRequest(format!("Invalid status: {}", dto.status))),
    };

    // Build source metadata
    let mut metadata = serde_json::json!({
        "authored": true,
        "claim_id": dto.id,
        "provenance": dto.provenance,
        "invariant": dto.invariant,
        "consequence": dto.consequence,
        "evidence": dto.evidence,
        "category": dto.category,
        "comparison": dto.comparison,
        "status": dto.status,
        "created_by": dto.created_by,
        "created_at": dto.created_at,
        "tool": "aphoria",
    });

    if let Some(ref supersedes) = dto.supersedes {
        metadata["supersedes"] = serde_json::json!(supersedes);
    }
    if let Some(ref updated_at) = dto.updated_at {
        metadata["updated_at"] = serde_json::json!(updated_at);
    }

    // Compute source hash from claim ID
    let source_hash = {
        let mut hasher = blake3::Hasher::new();
        hasher.update(b"claim:");
        hasher.update(dto.id.as_bytes());
        hasher.finalize().into()
    };

    // Subject is claim://concept_path/predicate
    let subject = format!("claim://{}/{}", dto.concept_path, dto.predicate);

    Ok(Assertion {
        subject,
        predicate: dto.predicate.clone(),
        object,
        parent_hash: dto.supersedes.as_ref().map(|sid| {
            let mut hasher = blake3::Hasher::new();
            hasher.update(b"claim:");
            hasher.update(sid.as_bytes());
            hasher.finalize().into()
        }),
        source_hash,
        source_class,
        visual_hash: None,
        epoch: None,
        source_metadata: serde_json::to_vec(&metadata).ok(),
        narrative: None,
        lifecycle,
        signatures: vec![], // Signatures added by ingestion pipeline
        confidence: 1.0,    // Authored claims have full confidence
        timestamp: std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .map(|d| d.as_secs())
            .unwrap_or(0),
        hlc_timestamp: Default::default(),
        vector: None,
    })
}

fn assertion_to_dto(assertion: &Assertion) -> Result<AuthoredClaimDto> {
    // Parse source metadata
    let metadata: serde_json::Value = assertion
        .source_metadata
        .as_ref()
        .and_then(|m| serde_json::from_slice(m).ok())
        .unwrap_or(serde_json::json!({}));

    // Extract claim ID
    let claim_id = metadata
        .get("claim_id")
        .and_then(|v| v.as_str())
        .ok_or_else(|| ApiError::Internal("Missing claim_id in metadata".to_string()))?
        .to_string();

    // Parse subject to extract concept_path
    // Subject format: claim://concept_path/predicate
    let concept_path = assertion
        .subject
        .strip_prefix("claim://")
        .ok_or_else(|| {
            ApiError::Internal("Invalid subject format: missing claim:// prefix".to_string())
        })?
        .rsplit_once('/')
        .map(|(cp, _)| cp)
        .ok_or_else(|| {
            ApiError::Internal("Invalid subject format: missing predicate separator".to_string())
        })?
        .to_string();

    // Convert object value
    let value = match &assertion.object {
        ObjectValue::Boolean(b) => AuthoredValueDto::Bool(*b),
        ObjectValue::Number(n) => AuthoredValueDto::Number(*n),
        ObjectValue::Text(s) => AuthoredValueDto::Text(s.clone()),
        ObjectValue::Reference(r) => AuthoredValueDto::Text(r.clone()),
    };

    // Convert lifecycle to status
    let status = match assertion.lifecycle {
        LifecycleStage::Proposed => "draft",
        LifecycleStage::Approved => "active",
        LifecycleStage::Deprecated => "deprecated",
        _ => "active",
    }
    .to_string();

    Ok(AuthoredClaimDto {
        id: claim_id,
        concept_path,
        predicate: assertion.predicate.clone(),
        value,
        comparison: metadata
            .get("comparison")
            .and_then(|v| v.as_str())
            .unwrap_or("equals")
            .to_string(),
        provenance: metadata.get("provenance").and_then(|v| v.as_str()).unwrap_or("").to_string(),
        invariant: metadata.get("invariant").and_then(|v| v.as_str()).unwrap_or("").to_string(),
        consequence: metadata.get("consequence").and_then(|v| v.as_str()).unwrap_or("").to_string(),
        authority_tier: source_class_to_tier_string(assertion.source_class),
        evidence: metadata
            .get("evidence")
            .and_then(|v| v.as_array())
            .map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect())
            .unwrap_or_default(),
        category: metadata.get("category").and_then(|v| v.as_str()).unwrap_or("").to_string(),
        status,
        supersedes: metadata.get("supersedes").and_then(|v| v.as_str()).map(String::from),
        created_by: metadata.get("created_by").and_then(|v| v.as_str()).unwrap_or("").to_string(),
        created_at: metadata.get("created_at").and_then(|v| v.as_str()).unwrap_or("").to_string(),
        updated_at: metadata.get("updated_at").and_then(|v| v.as_str()).map(String::from),
    })
}

fn parse_authority_tier(tier: &str) -> Result<stemedb_core::types::SourceClass> {
    use stemedb_core::types::SourceClass;

    match tier.to_lowercase().as_str() {
        "regulatory" => Ok(SourceClass::Regulatory),
        "clinical" | "rfc" => Ok(SourceClass::Clinical),
        "observational" => Ok(SourceClass::Observational),
        "team_policy" | "team-policy" => Ok(SourceClass::TeamPolicy),
        "expert" => Ok(SourceClass::Expert),
        "community" => Ok(SourceClass::Community),
        "anecdotal" => Ok(SourceClass::Anecdotal),
        _ => Err(ApiError::InvalidRequest(format!("Unknown authority tier: {}", tier))),
    }
}

fn source_class_to_tier_string(source_class: stemedb_core::types::SourceClass) -> String {
    use stemedb_core::types::SourceClass;

    match source_class {
        SourceClass::Regulatory => "regulatory",
        SourceClass::Clinical => "clinical",
        SourceClass::Observational => "observational",
        SourceClass::TeamPolicy => "team_policy",
        SourceClass::Expert => "expert",
        SourceClass::Community => "community",
        SourceClass::Anecdotal => "anecdotal",
    }
    .to_string()
}

/// Convert an authority tier string to its numeric tier number (0=regulatory, 5=anecdotal).
fn tier_string_to_number(tier: &str) -> Option<u8> {
    match tier.to_lowercase().as_str() {
        "regulatory" => Some(0),
        "clinical" | "rfc" => Some(1),
        "observational" | "team_policy" | "team-policy" => Some(2),
        "expert" => Some(3),
        "community" => Some(4),
        "anecdotal" => Some(5),
        _ => None,
    }
}

/// Convert an `AuthoredValueDto` to a display string.
fn value_to_string(value: &AuthoredValueDto) -> String {
    match value {
        AuthoredValueDto::Bool(b) => b.to_string(),
        AuthoredValueDto::Number(n) => n.to_string(),
        AuthoredValueDto::Text(s) => s.clone(),
    }
}

/// Simple wildcard glob matcher. `*` matches any number of path segments or characters.
///
/// - `*` alone matches everything
/// - `*/auth/*` matches `maxwell/wallet/auth/jwt`
/// - No leading wildcard anchors to the start of the string
fn matches_pattern(pattern: &str, subject: &str) -> bool {
    if pattern == "*" {
        return true;
    }
    let parts: Vec<&str> = pattern.split('*').collect();
    if parts.is_empty() {
        return true;
    }
    let mut remaining = subject;
    for (i, part) in parts.iter().enumerate() {
        if part.is_empty() {
            continue;
        }
        if i == 0 {
            // First non-empty part with no leading '*': must match at start
            if !remaining.starts_with(part) {
                return false;
            }
            remaining = &remaining[part.len()..];
        } else if i == parts.len() - 1 && !pattern.ends_with('*') {
            // Last part and pattern doesn't end with '*': must match at end
            return remaining.ends_with(part);
        } else {
            // Middle part: find anywhere in remaining
            if let Some(pos) = remaining.find(part) {
                remaining = &remaining[pos + part.len()..];
            } else {
                return false;
            }
        }
    }
    true
}

/// Scan all claim assertions from the store (shared by search and stats handlers).
async fn fetch_all_claims(state: &AppState) -> Result<Vec<AuthoredClaimDto>> {
    let subjects_prefix = b"\x00SUBJECTS:claim://";
    let subject_entries = state.store.scan_prefix(subjects_prefix).await?;

    let mut claims = Vec::new();
    for (key, _) in subject_entries {
        if let Some(subject) = key_codec::extract_subject_from_subjects_key(&key) {
            let subject_key = key_codec::subject_index_key(&subject);
            let hash_list = state.store.scan_prefix(&subject_key).await?;
            for (_, hash_bytes) in hash_list {
                let hash_hex = hex::encode(&hash_bytes);
                let assertion_key = key_codec::assertion_key(&subject, &hash_hex);
                if let Some(data) = state.store.get(&assertion_key).await? {
                    if let Ok(assertion) =
                        stemedb_core::serde::deserialize::<stemedb_core::types::Assertion>(&data)
                    {
                        if let Ok(dto) = assertion_to_dto(&assertion) {
                            claims.push(dto);
                        }
                    }
                }
            }
        }
    }
    Ok(claims)
}

// ============================================================================
// Search & Stats Handlers
// ============================================================================

/// Search claims using glob/wildcard patterns and tier/category/status filters.
#[utoipa::path(
    get,
    path = "/v1/claims/search",
    params(ClaimSearchQuery),
    responses(
        (status = 200, description = "Search results", body = Vec<AuthoredClaimDto>),
        (status = 500, description = "Internal server error")
    ),
    tag = "claims"
)]
pub async fn search_claims(
    State(state): State<AppState>,
    axum::extract::Query(query): axum::extract::Query<ClaimSearchQuery>,
) -> Result<Json<Vec<AuthoredClaimDto>>> {
    info!(
        concept_pattern = ?query.concept_pattern,
        predicate = ?query.predicate,
        category = ?query.category,
        max_tier = ?query.max_tier,
        status = ?query.status,
        "Searching claims in StemeDB"
    );

    let limit = query.limit.unwrap_or(50).min(200);
    let mut claims = fetch_all_claims(&state).await?;

    if let Some(ref pattern) = query.concept_pattern {
        claims.retain(|c| matches_pattern(pattern, &c.concept_path));
    }
    if let Some(ref predicate) = query.predicate {
        claims.retain(|c| c.predicate == *predicate || c.predicate.starts_with(predicate.as_str()));
    }
    if let Some(ref category) = query.category {
        claims.retain(|c| c.category == *category);
    }
    if let Some(max_tier) = query.max_tier {
        claims.retain(|c| {
            tier_string_to_number(&c.authority_tier).map(|t| t <= max_tier).unwrap_or(false)
        });
    }
    if let Some(ref status) = query.status {
        claims.retain(|c| c.status == *status);
    }

    claims.truncate(limit);
    Ok(Json(claims))
}

/// Get adoption statistics for a concept_path/predicate combination.
///
/// Accepts `concept_path` and `predicate` as query params to avoid
/// slash-in-path-param routing ambiguity.
#[utoipa::path(
    get,
    path = "/v1/claims/stats",
    params(
        ("concept_path" = String, Query, description = "Concept path to analyze"),
        ("predicate" = String, Query, description = "Predicate to analyze"),
    ),
    responses(
        (status = 200, description = "Claim statistics", body = ClaimStatsDto),
        (status = 400, description = "Missing concept_path or predicate"),
        (status = 500, description = "Internal server error")
    ),
    tag = "claims"
)]
pub async fn get_claim_stats(
    State(state): State<AppState>,
    axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>,
) -> Result<Json<ClaimStatsDto>> {
    let concept_path = params
        .get("concept_path")
        .ok_or_else(|| ApiError::InvalidRequest("Missing query param: concept_path".to_string()))?
        .clone();
    let predicate = params
        .get("predicate")
        .ok_or_else(|| ApiError::InvalidRequest("Missing query param: predicate".to_string()))?
        .clone();

    info!(concept_path, predicate, "Getting claim stats from StemeDB");

    let all_claims = fetch_all_claims(&state).await?;
    let matching: Vec<&AuthoredClaimDto> = all_claims
        .iter()
        .filter(|c| c.concept_path == concept_path && c.predicate == predicate)
        .collect();

    let mut by_tier: std::collections::HashMap<u8, usize> = std::collections::HashMap::new();
    let mut by_status: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
    let mut value_counts: std::collections::HashMap<String, usize> =
        std::collections::HashMap::new();
    let mut has_authoritative_backing = false;

    for claim in &matching {
        if let Some(tier) = tier_string_to_number(&claim.authority_tier) {
            *by_tier.entry(tier).or_insert(0) += 1;
            if tier <= 1 {
                has_authoritative_backing = true;
            }
        }
        *by_status.entry(claim.status.clone()).or_insert(0) += 1;
        *value_counts.entry(value_to_string(&claim.value)).or_insert(0) += 1;
    }

    let most_common_value =
        value_counts.into_iter().max_by_key(|(_, count)| *count).map(|(val, _)| val);

    Ok(Json(ClaimStatsDto {
        concept_path,
        predicate,
        matching_claims: matching.len(),
        by_tier,
        by_status,
        most_common_value,
        has_authoritative_backing,
    }))
}
