//! Handler functions for Source Registry operations.

use axum::{
    body::Body,
    extract::{Path, Query as AxumQuery, State},
    http::StatusCode,
    response::{IntoResponse, Response},
    Json,
};
use stemedb_core::types::{SourceRecord, SourceStatus};
use stemedb_storage::{
    GenericIndexStore, GenericSourceRegistry, IndexStore, KVStore, SourceRegistry,
};
use tracing::instrument;

use crate::{
    dto::{
        ErrorResponse, ExportImpactParams, ImpactExportRow, ListSourcesParams, ListSourcesResponse,
        QuarantineSourceRequest, QuarantineSourceResponse, RegisterSourceRequest,
        RegisterSourceResponse, RestoreSourceRequest, RestoreSourceResponse, SourceImpactResponse,
        SourceRecordDto, UpdateSourceStatusRequest, UpdateSourceStatusResponse,
    },
    error::{ApiError, Result},
    state::AppState,
};

use super::validation::{current_timestamp, validate_hash, validate_tier};

/// Register a new source in the registry.
///
/// Associates a human-readable label and metadata with a source content hash.
/// The hash should match one previously stored via POST /v1/source.
#[utoipa::path(
    post,
    path = "/v1/sources",
    request_body = RegisterSourceRequest,
    responses(
        (status = 201, description = "Source registered successfully", body = RegisterSourceResponse),
        (status = 400, description = "Invalid request (bad hex, invalid tier)", body = ErrorResponse),
        (status = 500, description = "Internal server error", body = ErrorResponse)
    ),
    tag = "source-registry"
)]
#[instrument(skip(state, req), fields(label = %req.label, tier = req.tier))]
pub async fn register_source(
    State(state): State<AppState>,
    Json(req): Json<RegisterSourceRequest>,
) -> Result<(StatusCode, Json<RegisterSourceResponse>)> {
    // Validate hash
    let hash = validate_hash(&req.hash)?;

    // Validate tier
    validate_tier(req.tier)?;

    // Validate label is not empty
    if req.label.trim().is_empty() {
        return Err(ApiError::InvalidRequest("Label cannot be empty".to_string()));
    }

    // Get timestamp
    let timestamp = current_timestamp();

    // Create the record
    let mut record = SourceRecord::new(hash, req.label.clone(), req.url, req.tier, timestamp);
    record.notes = req.notes;

    // Register in the store
    let registry = GenericSourceRegistry::new(state.store.clone());
    registry.register(&record).await?;

    tracing::info!(
        hash = %req.hash,
        label = %req.label,
        tier = req.tier,
        "Registered source in registry"
    );

    Ok((
        StatusCode::CREATED,
        Json(RegisterSourceResponse {
            hash: req.hash,
            label: req.label,
            status: "registered".to_string(),
        }),
    ))
}

/// Get a source by its hash.
///
/// Returns the full metadata for a registered source.
#[utoipa::path(
    get,
    path = "/v1/sources/{hash}",
    params(
        ("hash" = String, Path, description = "Source hash (hex-encoded, 64 chars)")
    ),
    responses(
        (status = 200, description = "Source found", body = SourceRecordDto),
        (status = 400, description = "Invalid hash format", body = ErrorResponse),
        (status = 404, description = "Source not found", body = ErrorResponse),
        (status = 500, description = "Internal server error", body = ErrorResponse)
    ),
    tag = "source-registry"
)]
#[instrument(skip(state), fields(hash = %hash))]
pub async fn get_source(
    State(state): State<AppState>,
    Path(hash): Path<String>,
) -> Result<Json<SourceRecordDto>> {
    // Validate and decode hash
    let hash_arr = validate_hash(&hash)?;

    // Lookup in registry
    let registry = GenericSourceRegistry::new(state.store.clone());
    let record = registry
        .get(&hash_arr)
        .await?
        .ok_or_else(|| ApiError::NotFound(format!("Source not found in registry: {}", hash)))?;

    Ok(Json(record.into()))
}

/// Update the status of a source.
///
/// Allows curators to deprecate or quarantine sources.
#[utoipa::path(
    patch,
    path = "/v1/sources/{hash}/status",
    params(
        ("hash" = String, Path, description = "Source hash (hex-encoded, 64 chars)")
    ),
    request_body = UpdateSourceStatusRequest,
    responses(
        (status = 200, description = "Status updated", body = UpdateSourceStatusResponse),
        (status = 400, description = "Invalid request", body = ErrorResponse),
        (status = 404, description = "Source not found", body = ErrorResponse),
        (status = 500, description = "Internal server error", body = ErrorResponse)
    ),
    tag = "source-registry"
)]
#[instrument(skip(state, req), fields(hash = %hash, new_status = %req.status))]
pub async fn update_source_status(
    State(state): State<AppState>,
    Path(hash): Path<String>,
    Json(req): Json<UpdateSourceStatusRequest>,
) -> Result<Json<UpdateSourceStatusResponse>> {
    // Validate and decode hash
    let hash_arr = validate_hash(&hash)?;

    // Parse status
    let status = SourceStatus::parse(&req.status).ok_or_else(|| {
        ApiError::InvalidRequest(format!(
            "Invalid status: '{}'. Must be 'active', 'deprecated', or 'quarantined'",
            req.status
        ))
    })?;

    // Get timestamp
    let timestamp = current_timestamp();

    // Update in registry
    let registry = GenericSourceRegistry::new(state.store.clone());
    registry.update_status(&hash_arr, status, timestamp).await.map_err(|e| {
        // Map NotFound storage error to API NotFound
        match e {
            stemedb_storage::StorageError::NotFound(_) => {
                ApiError::NotFound(format!("Source not found in registry: {}", hash))
            }
            other => ApiError::Storage(other),
        }
    })?;

    tracing::info!(
        hash = %hash,
        new_status = %req.status,
        "Updated source status"
    );

    Ok(Json(UpdateSourceStatusResponse {
        hash,
        status: req.status,
        message: "Status updated successfully".to_string(),
    }))
}

/// List or search sources in the registry.
///
/// Can filter by tier or search by label prefix.
#[utoipa::path(
    get,
    path = "/v1/sources",
    params(ListSourcesParams),
    responses(
        (status = 200, description = "Sources listed", body = ListSourcesResponse),
        (status = 400, description = "Invalid request", body = ErrorResponse),
        (status = 500, description = "Internal server error", body = ErrorResponse)
    ),
    tag = "source-registry"
)]
#[instrument(skip(state), fields(tier = ?params.tier, query = ?params.query, limit = params.limit))]
pub async fn list_sources(
    State(state): State<AppState>,
    AxumQuery(params): AxumQuery<ListSourcesParams>,
) -> Result<Json<ListSourcesResponse>> {
    // Validate limit
    let limit = params.limit.min(1000);

    let registry = GenericSourceRegistry::new(state.store.clone());

    let sources: Vec<SourceRecordDto> = if let Some(query) = &params.query {
        // Search by label
        registry.search(query, limit).await?.into_iter().map(Into::into).collect()
    } else if let Some(tier) = params.tier {
        // Filter by tier
        validate_tier(tier)?;
        registry.list_by_tier(tier, limit).await?.into_iter().map(Into::into).collect()
    } else {
        // List all (by scanning tier 0-5)
        let mut all = Vec::new();
        for tier in 0..=5 {
            let remaining = limit.saturating_sub(all.len());
            if remaining == 0 {
                break;
            }
            let tier_sources = registry.list_by_tier(tier, remaining).await?;
            all.extend(tier_sources);
        }
        all.into_iter().map(Into::into).collect()
    };

    let count = sources.len();
    Ok(Json(ListSourcesResponse { sources, count }))
}

// ============================================================================
// Impact Analysis Handlers (P3.1)
// ============================================================================

/// Get impact analysis for a source.
///
/// Shows how many assertions and queries would be affected by retracting this source.
/// This enables the "Aha Moment #2: Cascade invalidation when source retracted."
#[utoipa::path(
    get,
    path = "/v1/sources/{hash}/impact",
    params(
        ("hash" = String, Path, description = "Source hash (hex-encoded, 64 chars)")
    ),
    responses(
        (status = 200, description = "Impact analysis", body = SourceImpactResponse),
        (status = 400, description = "Invalid hash format", body = ErrorResponse),
        (status = 404, description = "Source not found", body = ErrorResponse),
        (status = 500, description = "Internal server error", body = ErrorResponse)
    ),
    tag = "source-registry"
)]
#[instrument(skip(state), fields(hash = %hash))]
pub async fn get_source_impact(
    State(state): State<AppState>,
    Path(hash): Path<String>,
) -> Result<Json<SourceImpactResponse>> {
    // Validate and decode hash
    let hash_arr = validate_hash(&hash)?;

    // Build the impact response
    let impact = build_impact_response(&state, &hash_arr, &hash).await?;

    Ok(Json(impact))
}

/// Quarantine a source (flag for exclusion from queries).
///
/// When preview=true, shows what would be affected without making changes.
/// When preview=false, sets the source status to Quarantined.
///
/// Quarantined sources are filtered out by lenses at query time,
/// preserving the append-only property of the assertion store.
#[utoipa::path(
    post,
    path = "/v1/sources/{hash}/quarantine",
    params(
        ("hash" = String, Path, description = "Source hash (hex-encoded, 64 chars)")
    ),
    request_body = QuarantineSourceRequest,
    responses(
        (status = 200, description = "Source quarantined or preview shown", body = QuarantineSourceResponse),
        (status = 400, description = "Invalid request", body = ErrorResponse),
        (status = 404, description = "Source not found", body = ErrorResponse),
        (status = 500, description = "Internal server error", body = ErrorResponse)
    ),
    tag = "source-registry"
)]
#[instrument(skip(state, req), fields(hash = %hash, preview = req.preview))]
pub async fn quarantine_source(
    State(state): State<AppState>,
    Path(hash): Path<String>,
    Json(req): Json<QuarantineSourceRequest>,
) -> Result<Json<QuarantineSourceResponse>> {
    // Validate and decode hash
    let hash_arr = validate_hash(&hash)?;

    // Get the registry
    let registry = GenericSourceRegistry::new(state.store.clone());

    // Check that source exists and get current status
    let record = registry
        .get(&hash_arr)
        .await?
        .ok_or_else(|| ApiError::NotFound(format!("Source not found in registry: {}", hash)))?;

    // Build impact response first
    let impact = build_impact_response(&state, &hash_arr, &hash).await?;

    // If preview mode, return without making changes
    if req.preview {
        let assertion_count = impact.assertion_count;
        let agent_count = impact.affected_agents.len();
        return Ok(Json(QuarantineSourceResponse {
            source_hash: hash.clone(),
            status: record.status.as_str().to_string(),
            impact,
            preview: true,
            message: format!(
                "Preview: Quarantining this source would affect {} assertions from {} agents",
                assertion_count, agent_count
            ),
        }));
    }

    // Update status to Quarantined
    let timestamp = current_timestamp();
    registry.update_status(&hash_arr, SourceStatus::Quarantined, timestamp).await.map_err(|e| {
        match e {
            stemedb_storage::StorageError::NotFound(_) => {
                ApiError::NotFound(format!("Source not found in registry: {}", hash))
            }
            other => ApiError::Storage(other),
        }
    })?;

    let assertion_count = impact.assertion_count;
    let agent_count = impact.affected_agents.len();

    tracing::info!(
        hash = %hash,
        assertion_count = assertion_count,
        agent_count = agent_count,
        reason = ?req.reason,
        "Source quarantined"
    );

    Ok(Json(QuarantineSourceResponse {
        source_hash: hash,
        status: "quarantined".to_string(),
        impact,
        preview: false,
        message: format!(
            "Source quarantined. {} assertions from {} agents will be filtered from queries.",
            assertion_count, agent_count
        ),
    }))
}

/// Restore a quarantined source.
///
/// Sets the source status back to Active, allowing assertions that cite it
/// to be included in query results again.
#[utoipa::path(
    post,
    path = "/v1/sources/{hash}/restore",
    params(
        ("hash" = String, Path, description = "Source hash (hex-encoded, 64 chars)")
    ),
    request_body = RestoreSourceRequest,
    responses(
        (status = 200, description = "Source restored", body = RestoreSourceResponse),
        (status = 400, description = "Invalid request", body = ErrorResponse),
        (status = 404, description = "Source not found", body = ErrorResponse),
        (status = 409, description = "Source is not quarantined", body = ErrorResponse),
        (status = 500, description = "Internal server error", body = ErrorResponse)
    ),
    tag = "source-registry"
)]
#[instrument(skip(state, req), fields(hash = %hash))]
pub async fn restore_source(
    State(state): State<AppState>,
    Path(hash): Path<String>,
    Json(req): Json<RestoreSourceRequest>,
) -> Result<Json<RestoreSourceResponse>> {
    // Validate and decode hash
    let hash_arr = validate_hash(&hash)?;

    // Get the registry
    let registry = GenericSourceRegistry::new(state.store.clone());

    // Check that source exists and is quarantined
    let record = registry
        .get(&hash_arr)
        .await?
        .ok_or_else(|| ApiError::NotFound(format!("Source not found in registry: {}", hash)))?;

    if record.status != SourceStatus::Quarantined {
        return Err(ApiError::Conflict(format!(
            "Source is not quarantined (current status: {})",
            record.status.as_str()
        )));
    }

    // Get assertion count before restoring
    let index_store = GenericIndexStore::new(state.store.clone());
    let assertion_count =
        index_store.count_by_source(&hash_arr).await.map_err(ApiError::Storage)?;

    // Update status to Active
    let timestamp = current_timestamp();
    registry.update_status(&hash_arr, SourceStatus::Active, timestamp).await.map_err(
        |e| match e {
            stemedb_storage::StorageError::NotFound(_) => {
                ApiError::NotFound(format!("Source not found in registry: {}", hash))
            }
            other => ApiError::Storage(other),
        },
    )?;

    tracing::info!(
        hash = %hash,
        restored_assertions = assertion_count,
        reason = ?req.reason,
        "Source restored"
    );

    Ok(Json(RestoreSourceResponse {
        source_hash: hash,
        status: "active".to_string(),
        restored_assertions: assertion_count,
        message: format!(
            "Source restored. {} assertions can now be included in query results.",
            assertion_count
        ),
    }))
}

// ============================================================================
// Export Endpoint (P3.2 Cascade Flagging)
// ============================================================================

/// Export affected assertions for regulatory documentation.
///
/// Returns a list of assertions citing the specified source, formatted
/// for compliance audit trails. Supports CSV and JSON formats.
#[utoipa::path(
    get,
    path = "/v1/sources/{hash}/impact/export",
    params(
        ("hash" = String, Path, description = "Source hash (hex-encoded, 64 chars)"),
        ExportImpactParams,
    ),
    responses(
        (status = 200, description = "CSV export", content_type = "text/csv"),
        (status = 200, description = "JSON export", body = Vec<ImpactExportRow>),
        (status = 400, description = "Invalid hash format", body = ErrorResponse),
        (status = 500, description = "Internal server error", body = ErrorResponse),
    ),
    tag = "source-registry"
)]
#[instrument(skip(state), fields(hash = %hash, format = %params.format))]
pub async fn export_source_impact(
    State(state): State<AppState>,
    Path(hash): Path<String>,
    AxumQuery(params): AxumQuery<ExportImpactParams>,
) -> Result<Response> {
    let hash_arr = validate_hash(&hash)?;

    // Get assertion hashes from source index
    let index_store = GenericIndexStore::new(state.store.clone());
    let assertion_hashes = index_store.get_by_source(&hash_arr).await.map_err(ApiError::Storage)?;

    // Build export rows by reading each assertion
    let rows = build_export_rows(&state, &assertion_hashes).await?;

    match params.format.to_lowercase().as_str() {
        "csv" => {
            let csv = build_csv(&rows);
            Response::builder()
                .status(StatusCode::OK)
                .header("Content-Type", "text/csv; charset=utf-8")
                .header(
                    "Content-Disposition",
                    format!("attachment; filename=\"impact-{}.csv\"", &hash[..8.min(hash.len())]),
                )
                .body(Body::from(csv))
                .map_err(|e| ApiError::Internal(format!("Response build failed: {}", e)))
        }
        _ => Ok(Json(rows).into_response()),
    }
}

/// Build export rows from assertion hashes.
async fn build_export_rows(
    state: &AppState,
    assertion_hashes: &[[u8; 32]],
) -> Result<Vec<ImpactExportRow>> {
    let mut rows = Vec::with_capacity(assertion_hashes.len().min(1000));

    // Limit to 1000 rows for performance
    for assertion_hash in assertion_hashes.iter().take(1000) {
        // Look up the subject from the reverse index
        let reverse_key =
            stemedb_storage::key_codec::hash_subject_key(&hex::encode(assertion_hash));

        let subject_bytes = match state.store.get(&reverse_key).await {
            Ok(Some(bytes)) => bytes,
            _ => continue, // Skip if we can't find the subject
        };

        let subject = match String::from_utf8(subject_bytes) {
            Ok(s) => s,
            _ => continue,
        };

        // Read the assertion
        let assertion_key =
            stemedb_storage::key_codec::assertion_key(&subject, &hex::encode(assertion_hash));

        let assertion_data = match state.store.get(&assertion_key).await {
            Ok(Some(data)) => data,
            _ => continue,
        };

        let assertion: stemedb_core::types::Assertion =
            match stemedb_core::serde::deserialize(&assertion_data) {
                Ok(a) => a,
                _ => continue,
            };

        // Extract first agent ID from signatures
        let agent_id = assertion
            .signatures
            .first()
            .map(|sig| hex::encode(sig.agent_id))
            .unwrap_or_else(|| "unknown".to_string());

        rows.push(ImpactExportRow {
            assertion_hash: hex::encode(assertion_hash),
            subject: assertion.subject,
            predicate: assertion.predicate,
            agent_id,
            timestamp: assertion.timestamp,
            lifecycle: format!("{:?}", assertion.lifecycle),
            confidence: assertion.confidence,
        });
    }

    Ok(rows)
}

/// Build CSV from export rows.
fn build_csv(rows: &[ImpactExportRow]) -> String {
    let mut csv =
        String::from("assertion_hash,subject,predicate,agent_id,timestamp,lifecycle,confidence\n");
    for row in rows {
        csv.push_str(&format!(
            "{},{},{},{},{},{},{:.3}\n",
            row.assertion_hash,
            escape_csv(&row.subject),
            escape_csv(&row.predicate),
            row.agent_id,
            row.timestamp,
            row.lifecycle,
            row.confidence
        ));
    }
    csv
}

/// Escape a CSV field (wrap in quotes if needed).
fn escape_csv(s: &str) -> String {
    if s.contains(',') || s.contains('"') || s.contains('\n') {
        format!("\"{}\"", s.replace('"', "\"\""))
    } else {
        s.to_string()
    }
}

/// Build a SourceImpactResponse for the given source hash.
async fn build_impact_response(
    state: &AppState,
    hash_arr: &[u8; 32],
    hash_hex: &str,
) -> Result<SourceImpactResponse> {
    // Get source metadata from registry (optional - source may not be registered)
    let registry = GenericSourceRegistry::new(state.store.clone());
    let record = registry.get(hash_arr).await?;

    let label = record.as_ref().map(|r| r.label.clone());
    let status = record
        .as_ref()
        .map(|r| r.status.as_str().to_string())
        .unwrap_or_else(|| "unknown".to_string());

    // Get assertions citing this source from the reverse index
    let index_store = GenericIndexStore::new(state.store.clone());
    let assertion_hashes = index_store.get_by_source(hash_arr).await.map_err(ApiError::Storage)?;

    let assertion_count = assertion_hashes.len();

    // Convert to hex strings, limited to 100
    let affected_assertions: Vec<String> =
        assertion_hashes.iter().take(100).map(hex::encode).collect();

    // Extract unique agent IDs from affected assertions
    // This requires reading each assertion to get its signatures
    // For efficiency, we'll limit the scan and note it's an estimate
    let mut affected_agents: Vec<String> = Vec::new();
    let mut seen_agents = std::collections::HashSet::new();

    // Only scan up to 100 assertions for agent extraction
    for assertion_hash in assertion_hashes.iter().take(100) {
        // Try to read the assertion to get agent signatures
        // Look up the subject from the reverse index
        let reverse_key =
            stemedb_storage::key_codec::hash_subject_key(&hex::encode(assertion_hash));
        if let Ok(Some(subject_bytes)) = state.store.get(&reverse_key).await {
            if let Ok(subject) = String::from_utf8(subject_bytes) {
                // Try to read the assertion
                let assertion_key = stemedb_storage::key_codec::assertion_key(
                    &subject,
                    &hex::encode(assertion_hash),
                );
                if let Ok(Some(data)) = state.store.get(&assertion_key).await {
                    if let Ok(assertion) =
                        stemedb_core::serde::deserialize::<stemedb_core::types::Assertion>(&data)
                    {
                        for sig in &assertion.signatures {
                            let agent_hex = hex::encode(sig.agent_id);
                            if seen_agents.insert(agent_hex.clone()) {
                                affected_agents.push(agent_hex);
                            }
                        }
                    }
                }
            }
        }
    }

    // Query count: best-effort from audit store
    // For now, return 0 - this would require scanning audit logs
    // TODO: Implement audit log scanning for affected queries
    let query_count = 0;

    let summary = if assertion_count == 0 {
        "This source has no citations".to_string()
    } else {
        format!(
            "This will affect {} assertion{} from {} agent{}",
            assertion_count,
            if assertion_count == 1 { "" } else { "s" },
            affected_agents.len(),
            if affected_agents.len() == 1 { "" } else { "s" }
        )
    };

    Ok(SourceImpactResponse {
        source_hash: hash_hex.to_string(),
        label,
        status,
        assertion_count,
        affected_assertions,
        query_count,
        affected_agents,
        summary,
    })
}
