//! Insights endpoint handlers

#[cfg(feature = "ml-features")]
use crate::server::services::vector_database::{VectorDatabase, VectorSearchResult};
#[cfg(feature = "ml-features")]
use anyhow::anyhow;
use anyhow::Result;
use axum::{
    extract::{Extension, Json},
    response::Json as ResponseJson,
};
use uuid::Uuid;

use crate::server::types::{
    AddInsightRequest, ApiError, BaseResponse, GetInsightRequest, GetInsightResponse, InsightData,
    InsightSummary, ListInsightsResponse, ListTopicsResponse, PinInsightRequest,
    RemoveInsightRequest, SearchRequest, SearchResponse, SearchResultData, UpdateInsightRequest,
};
use crate::server::{
    middleware::RequestContext,
    models::insight::{self, AccessType},
};

/// PUT /insights/update - Update an existing insight
pub async fn update_insight(
    Extension(context): Extension<RequestContext>,
    Json(request): Json<UpdateInsightRequest>,
) -> Result<ResponseJson<BaseResponse<()>>, (axum::http::StatusCode, ResponseJson<BaseResponse<()>>)>
{
    let transaction_id = Uuid::new_v4();

    let mut insight_data = load_existing_insight(&request, transaction_id)?;
    update_insight_with_embedding(&context, &mut insight_data, &request, transaction_id).await
}

/// Load existing insight or return not found error
fn load_existing_insight(
    request: &UpdateInsightRequest,
    transaction_id: Uuid,
) -> Result<insight::Insight, (axum::http::StatusCode, ResponseJson<BaseResponse<()>>)> {
    insight::load(&request.topic, &request.name)
        .map_err(|e| create_insight_not_found_error(e, transaction_id))
}

/// Update insight and regenerate embedding
async fn update_insight_with_embedding(
    context: &RequestContext,
    insight_data: &mut insight::Insight,
    request: &UpdateInsightRequest,
    transaction_id: Uuid,
) -> Result<ResponseJson<BaseResponse<()>>, (axum::http::StatusCode, ResponseJson<BaseResponse<()>>)>
{
    perform_insight_update(insight_data, request, transaction_id)?;
    attempt_embedding_update(context, insight_data).await;

    Ok(ResponseJson(BaseResponse::success((), transaction_id)))
}

/// Perform the actual insight update operation
fn perform_insight_update(
    insight_data: &mut insight::Insight,
    request: &UpdateInsightRequest,
    transaction_id: Uuid,
) -> Result<(), (axum::http::StatusCode, ResponseJson<BaseResponse<()>>)> {
    insight::update(
        insight_data,
        request.overview.as_deref(),
        request.details.as_deref(),
    )
    .map_err(|e| create_insight_update_error(e, transaction_id))
}

/// Attempt to update embedding (non-fatal if fails)
async fn attempt_embedding_update(context: &RequestContext, insight: &insight::Insight) {
    match generate_and_store_embedding(context, insight).await {
        Ok(_) => {
            log_embedding_update_success(context, insight).await;
        }
        Err(e) => {
            log_embedding_update_warning(context, e).await;
        }
    }
}

/// Log successful insight update with embedding
async fn log_embedding_update_success(context: &RequestContext, insight: &insight::Insight) {
    context
        .log_success(
            &format!(
                "Successfully updated insight {}/{} with new embedding",
                insight.topic, insight.name
            ),
            "insights-api",
        )
        .await;
}

/// Log embedding update warning (non-fatal)
async fn log_embedding_update_warning(context: &RequestContext, error: anyhow::Error) {
    context
        .log_warn(
            &format!("Insight updated but embedding update failed: {error}"),
            "insights-api",
        )
        .await;
}

/// Create error response for insight not found
fn create_insight_not_found_error(
    error: anyhow::Error,
    transaction_id: Uuid,
) -> (axum::http::StatusCode, ResponseJson<BaseResponse<()>>) {
    let api_error = ApiError::new("insight_not_found", &format!("Insight not found: {error}"));
    (
        axum::http::StatusCode::NOT_FOUND,
        ResponseJson(BaseResponse::<()>::error(vec![api_error], transaction_id)),
    )
}

/// Create error response for insight update failure
fn create_insight_update_error(
    error: anyhow::Error,
    transaction_id: Uuid,
) -> (axum::http::StatusCode, ResponseJson<BaseResponse<()>>) {
    let api_error = ApiError::new(
        "insight_update_failed",
        &format!("Failed to update insight: {error}"),
    );
    (
        axum::http::StatusCode::INTERNAL_SERVER_ERROR,
        ResponseJson(BaseResponse::<()>::error(vec![api_error], transaction_id)),
    )
}

/// PUT /insights/pin - Pin an insight to protect from pruning
pub async fn pin_insight(
    Extension(context): Extension<RequestContext>,
    Json(request): Json<PinInsightRequest>,
) -> Result<ResponseJson<BaseResponse<()>>, (axum::http::StatusCode, ResponseJson<BaseResponse<()>>)>
{
    let transaction_id = Uuid::new_v4();

    let mut insight_data = insight::load(&request.topic, &request.name)
        .map_err(|e| create_insight_not_found_error(e, transaction_id))?;

    insight::pin(&mut insight_data).map_err(|e| create_pin_error(e, transaction_id))?;

    context
        .log_success(
            &format!("Pinned insight {}/{}", request.topic, request.name),
            "insights-api",
        )
        .await;

    Ok(ResponseJson(BaseResponse::success((), transaction_id)))
}

/// DELETE /insights/unpin - Unpin an insight
pub async fn unpin_insight(
    Extension(context): Extension<RequestContext>,
    Json(request): Json<PinInsightRequest>,
) -> Result<ResponseJson<BaseResponse<()>>, (axum::http::StatusCode, ResponseJson<BaseResponse<()>>)>
{
    let transaction_id = Uuid::new_v4();

    let mut insight_data = insight::load(&request.topic, &request.name)
        .map_err(|e| create_insight_not_found_error(e, transaction_id))?;

    insight::unpin(&mut insight_data).map_err(|e| create_pin_error(e, transaction_id))?;

    context
        .log_success(
            &format!("Unpinned insight {}/{}", request.topic, request.name),
            "insights-api",
        )
        .await;

    Ok(ResponseJson(BaseResponse::success((), transaction_id)))
}

fn create_pin_error(
    error: anyhow::Error,
    transaction_id: Uuid,
) -> (axum::http::StatusCode, ResponseJson<BaseResponse<()>>) {
    let api_error = ApiError::new(
        "insight_pin_failed",
        &format!("Failed to update pin status: {error}"),
    );
    (
        axum::http::StatusCode::INTERNAL_SERVER_ERROR,
        ResponseJson(BaseResponse::<()>::error(vec![api_error], transaction_id)),
    )
}

/// DELETE /insights/remove - Remove an insight
pub async fn remove_insight(
    Extension(context): Extension<RequestContext>,
    Json(request): Json<RemoveInsightRequest>,
) -> Result<ResponseJson<BaseResponse<()>>, (axum::http::StatusCode, ResponseJson<BaseResponse<()>>)>
{
    let transaction_id = Uuid::new_v4();

    let insight_to_delete = load_insight_for_deletion(&request, transaction_id)?;
    delete_insight_with_embedding(&context, &insight_to_delete, &request, transaction_id).await
}

/// Load insight for deletion or return not found error
fn load_insight_for_deletion(
    request: &RemoveInsightRequest,
    transaction_id: Uuid,
) -> Result<insight::Insight, (axum::http::StatusCode, ResponseJson<BaseResponse<()>>)> {
    insight::load(&request.topic, &request.name)
        .map_err(|e| create_insight_not_found_error(e, transaction_id))
}

/// Delete insight and its embedding
async fn delete_insight_with_embedding(
    context: &RequestContext,
    insight_to_delete: &insight::Insight,
    request: &RemoveInsightRequest,
    transaction_id: Uuid,
) -> Result<ResponseJson<BaseResponse<()>>, (axum::http::StatusCode, ResponseJson<BaseResponse<()>>)>
{
    perform_insight_deletion(insight_to_delete, transaction_id)?;
    attempt_embedding_deletion(context, request).await;

    Ok(ResponseJson(BaseResponse::success((), transaction_id)))
}

/// Perform the actual insight deletion operation
fn perform_insight_deletion(
    insight_to_delete: &insight::Insight,
    transaction_id: Uuid,
) -> Result<(), (axum::http::StatusCode, ResponseJson<BaseResponse<()>>)> {
    insight::delete(insight_to_delete).map_err(|e| create_insight_removal_error(e, transaction_id))
}

/// Attempt to delete embedding (non-fatal if fails)
#[cfg(feature = "ml-features")]
async fn attempt_embedding_deletion(context: &RequestContext, request: &RemoveInsightRequest) {
    match context
        .vector_db
        .delete_embedding(&request.topic, &request.name)
        .await
    {
        Ok(_) => {
            log_embedding_deletion_success(context, request).await;
        }
        Err(e) => {
            log_embedding_deletion_warning(context, e).await;
        }
    }
}

/// Attempt to delete embedding (no-op without ml-features)
#[cfg(not(feature = "ml-features"))]
async fn attempt_embedding_deletion(_context: &RequestContext, _request: &RemoveInsightRequest) {
    // No-op: embeddings not available without ml-features
}

/// Log successful insight deletion with embedding
#[cfg(feature = "ml-features")]
async fn log_embedding_deletion_success(context: &RequestContext, request: &RemoveInsightRequest) {
    context
        .log_success(
            &format!(
                "Successfully deleted insight {}/{} and its embedding",
                request.topic, request.name
            ),
            "insights-api",
        )
        .await;
}

/// Log embedding deletion warning (non-fatal)
#[cfg(feature = "ml-features")]
async fn log_embedding_deletion_warning(context: &RequestContext, error: anyhow::Error) {
    context
        .log_warn(
            &format!("Insight deleted but embedding deletion failed: {error}"),
            "insights-api",
        )
        .await;
}

/// Create error response for insight removal failure
fn create_insight_removal_error(
    error: anyhow::Error,
    transaction_id: Uuid,
) -> (axum::http::StatusCode, ResponseJson<BaseResponse<()>>) {
    let api_error = ApiError::new(
        "insight_remove_failed",
        &format!("Failed to remove insight: {error}"),
    );
    (
        axum::http::StatusCode::INTERNAL_SERVER_ERROR,
        ResponseJson(BaseResponse::<()>::error(vec![api_error], transaction_id)),
    )
}

/// DELETE /insights/clear - Clear all insights
pub async fn clear_insights(
) -> Result<ResponseJson<BaseResponse<()>>, (axum::http::StatusCode, ResponseJson<BaseResponse<()>>)>
{
    let transaction_id = Uuid::new_v4();

    // TODO: Implement clear insights using existing logic
    Ok(ResponseJson(BaseResponse::success((), transaction_id)))
}

/// DELETE /insights/index - Re-index all insights (delete existing index and rebuild)
pub async fn reindex(
    Extension(context): Extension<RequestContext>,
) -> Result<ResponseJson<BaseResponse<()>>, (axum::http::StatusCode, ResponseJson<BaseResponse<()>>)>
{
    let transaction_id = Uuid::new_v4();

    context
        .log_info("Starting insight re-indexing process", "insights-api")
        .await;

    // Spawn fire-and-forget task to handle re-indexing
    tokio::spawn(async move {
        if let Err(e) = perform_reindexing(context.clone()).await {
            context
                .log_error(&format!("Re-indexing failed: {e}"), "insights-api")
                .await;
        }
    });

    // Return immediately - don't wait for re-indexing to complete
    Ok(ResponseJson(BaseResponse::success((), transaction_id)))
}

/// Perform the actual re-indexing process (fire-and-forget)
async fn perform_reindexing(context: RequestContext) -> Result<()> {
    let all_insights = load_all_insights_for_reindexing(&context).await?;
    clear_existing_embeddings(&context).await?;
    let stats = process_insights_for_embedding(&context, &all_insights).await;
    log_reindexing_completion(&context, &stats).await;
    Ok(())
}

/// Load all insights from filesystem for re-indexing
async fn load_all_insights_for_reindexing(
    context: &RequestContext,
) -> Result<Vec<insight::Insight>> {
    context
        .log_info("Loading all insights for re-indexing", "insights-reindex")
        .await;

    let all_insights = insight::get_insights(None).map_err(|e| {
        // Log error but let caller handle the Result
        tokio::spawn({
            let context = context.clone();
            let error_msg = format!("Failed to load insights: {e}");
            async move {
                context.log_error(&error_msg, "insights-reindex").await;
            }
        });
        e
    })?;

    let total_insights = all_insights.len();
    context
        .log_info(
            &format!("Found {total_insights} insights to re-index"),
            "insights-reindex",
        )
        .await;

    Ok(all_insights)
}

/// Clear existing embeddings from database to start fresh
#[cfg(feature = "ml-features")]
async fn clear_existing_embeddings(context: &RequestContext) -> Result<()> {
    context
        .log_info(
            "Starting clean slate database recreation",
            "insights-reindex",
        )
        .await;

    // Detect current embedding model dimension
    let embedding_dimension =
        match crate::server::services::embeddings::detect_embedding_dimension().await {
            Ok(dim) => {
                context
                    .log_info(
                        &format!("Detected embedding model dimension: {dim}"),
                        "insights-reindex",
                    )
                    .await;
                dim
            }
            Err(e) => {
                context
                    .log_error(
                        &format!("Failed to detect embedding dimension, using default 768: {e}"),
                        "insights-reindex",
                    )
                    .await;
                768 // Fallback to default
            }
        };

    // Reshape the database with the correct schema
    context
        .vector_db
        .reshape_database(embedding_dimension)
        .await?;
    context
        .log_info("Database reshape completed", "insights-reindex")
        .await;

    Ok(())
}

/// Clear existing embeddings (no-op without ml-features)
#[cfg(not(feature = "ml-features"))]
async fn clear_existing_embeddings(context: &RequestContext) -> Result<()> {
    context
        .log_info(
            "Skipping embedding clearing (no ML features)",
            "insights-reindex",
        )
        .await;
    Ok(())
}

/// Statistics for tracking re-indexing progress
#[derive(Debug, Default)]
struct ReindexingStats {
    embedded: usize,
    errors: usize,
    total: usize,
}

/// Process all insights for embedding generation
async fn process_insights_for_embedding(
    context: &RequestContext,
    insights: &[insight::Insight],
) -> ReindexingStats {
    let mut stats = ReindexingStats {
        total: insights.len(),
        ..Default::default()
    };

    for (index, insight) in insights.iter().enumerate() {
        log_progress_if_needed(context, index, &stats).await;

        match generate_and_store_embedding(context, insight).await {
            Ok(_) => stats.embedded += 1,
            Err(e) => {
                stats.errors += 1;
                context
                    .log_warn(
                        &format!(
                            "Failed to generate embedding for {}/{}: {}",
                            insight.topic, insight.name, e
                        ),
                        "insights-reindex",
                    )
                    .await;
            }
        }

        // Rate limiting to prevent overwhelming embedding service
        tokio::time::sleep(std::time::Duration::from_millis(10)).await;
    }

    stats
}

/// Log progress periodically during processing
async fn log_progress_if_needed(context: &RequestContext, index: usize, stats: &ReindexingStats) {
    if (index + 1).is_multiple_of(10) || index == stats.total - 1 {
        context
            .log_info(
                &format!(
                    "Re-indexing progress: {}/{} (embedded: {}, errors: {})",
                    index + 1,
                    stats.total,
                    stats.embedded,
                    stats.errors
                ),
                "insights-reindex",
            )
            .await;
    }
}

/// Log final completion statistics
async fn log_reindexing_completion(context: &RequestContext, stats: &ReindexingStats) {
    context
        .log_success(
            &format!(
                "Re-indexing completed: {}/{} insights embedded successfully, {} errors",
                stats.embedded, stats.total, stats.errors
            ),
            "insights-reindex",
        )
        .await;
}

/// Generate embedding for an insight and store it in LanceDB
#[cfg(feature = "ml-features")]
async fn generate_and_store_embedding(
    context: &RequestContext,
    insight: &insight::Insight,
) -> Result<()> {
    // Create document content and title for proper EmbeddingGemma formatting
    let document_title = format!("{}/{}", insight.topic, insight.name);
    let document_content = format!("{} {}", insight.overview, insight.details);

    // Generate embedding using proper document format for EmbeddingGemma
    let embedding = crate::server::services::embeddings::create_document_embedding(
        &document_content,
        Some(&document_title),
    )
    .await
    .map_err(|e| anyhow!("Failed to generate document embedding: {}", e))?;

    // Store the properly formatted text that was actually embedded
    let formatted_embedding_text = format!("title: {document_title} | text: {document_content}");

    // Create insight with embedding data, preserving existing temporal metadata
    let mut insight_with_embedding = insight.clone();
    insight_with_embedding.embedding_version = Some("embeddinggemma-300m".to_string());
    insight_with_embedding.embedding = Some(embedding.clone());
    insight_with_embedding.embedding_text = Some(formatted_embedding_text);
    insight_with_embedding.embedding_computed = Some(chrono::Utc::now());

    // Store in vector database
    context
        .vector_db
        .store_embedding(&insight_with_embedding)
        .await?;

    // Update the insight file with embedding metadata
    insight::save_existing(&insight_with_embedding)?;

    Ok(())
}

/// Generate embedding for an insight and store it in LanceDB (no-op without ml-features)
#[cfg(not(feature = "ml-features"))]
async fn generate_and_store_embedding(
    _context: &RequestContext,
    _insight: &insight::Insight,
) -> Result<()> {
    // No-op: ML features not available
    Ok(())
}

/// Perform vector similarity search with reranking using LanceDB
#[cfg(feature = "ml-features")]
async fn perform_vector_search(
    context: &RequestContext,
    request: &SearchRequest,
    search_options: &crate::server::services::search::SearchOptions,
) -> Result<Vec<SearchResultData>> {
    let query_text = request.terms.join(" ");

    let query_embedding = embed_query(&query_text).await?;
    let similar_results = initial_search(context, &query_embedding).await?;
    let reranked_results =
        rerank_results(context, &query_text, similar_results, search_options).await;
    let final_results = limit_results(reranked_results);

    Ok(final_results)
}

/// Generate query embedding with proper error handling
#[cfg(feature = "ml-features")]
async fn embed_query(query_text: &str) -> Result<Vec<f32>> {
    crate::server::services::embeddings::create_query_embedding(query_text)
        .await
        .map_err(|e| anyhow!("Failed to generate query embedding: {}", e))
}

/// Retrieve initial candidates using permissive settings
#[cfg(feature = "ml-features")]
async fn initial_search(
    context: &RequestContext,
    query_embedding: &[f32],
) -> Result<Vec<crate::server::services::vector_database::VectorSearchResult>> {
    let initial_limit = get_initial_search_limit();
    let initial_threshold = Some(get_initial_search_threshold());
    let results = context
        .vector_db
        .search_similar(query_embedding, initial_limit, initial_threshold)
        .await?;

    if results.is_empty() {
        return Ok(vec![]);
    }

    Ok(results)
}

/// Rerank search candidates using semantic similarity
#[cfg(feature = "ml-features")]
async fn rerank_results(
    context: &RequestContext,
    query_text: &str,
    similar_results: Vec<crate::server::services::vector_database::VectorSearchResult>,
    search_options: &crate::server::services::search::SearchOptions,
) -> Vec<SearchResultData> {
    let mut reranked_results = Vec::new();

    for result in similar_results {
        if let Some(search_result) =
            score_single_result(context, query_text, result, search_options).await
        {
            reranked_results.push(search_result);
        }
    }

    reranked_results
}

/// Rerank a single candidate result
#[cfg(feature = "ml-features")]
async fn score_single_result(
    context: &RequestContext,
    query_text: &str,
    result: VectorSearchResult,
    search_options: &crate::server::services::search::SearchOptions,
) -> Option<SearchResultData> {
    match insight::load(&result.topic, &result.name) {
        Ok(full_insight) => {
            if !crate::server::services::search::matches_date_range(&full_insight, search_options) {
                return None;
            }

            let doc_text = format!(
                "{} {} {} {}",
                result.topic, result.name, result.overview, result.details
            );
            let base_score = compute_relevance_score(query_text, &doc_text, &result).await;
            let score = base_score * crate::server::services::search::usage_boost(&full_insight);

            Some(SearchResultData {
                topic: result.topic,
                name: result.name,
                overview: result.overview,
                details: result.details,
                score,
                created_at: full_insight.created_at,
                updated_at: full_insight.last_updated,
            })
        }
        Err(e) => {
            tokio::spawn({
                let context = context.clone();
                let topic = result.topic.clone();
                let name = result.name.clone();
                async move {
                    context
                        .log_warn(
                            &format!("Failed to load full insight {topic}/{name}: {e}"),
                            "insights-search",
                        )
                        .await;
                }
            });
            None
        }
    }
}

/// Compute reranking score with fallback
#[cfg(feature = "ml-features")]
async fn compute_relevance_score(
    query_text: &str,
    doc_text: &str,
    result: &crate::server::services::vector_database::VectorSearchResult,
) -> f32 {
    match crate::server::services::embeddings::score_relevance(query_text, doc_text).await {
        Ok(score) => score,
        Err(e) => {
            bentley::warn!(&format!(
                "Reranking failed for {}/{}: {}, using original score",
                result.topic, result.name, e
            ));
            result.similarity
        }
    }
}

/// Sort and limit reranked search results
#[cfg(feature = "ml-features")]
fn limit_results(mut reranked_results: Vec<SearchResultData>) -> Vec<SearchResultData> {
    reranked_results.sort_by(|a, b| {
        b.score
            .partial_cmp(&a.score)
            .unwrap_or(std::cmp::Ordering::Equal)
    });

    let final_limit = get_rerank_limit();
    reranked_results.truncate(final_limit);
    reranked_results
}

/// Perform vector similarity search using LanceDB (no-op without ml-features)
#[cfg(not(feature = "ml-features"))]
async fn perform_vector_search(
    _context: &RequestContext,
    _request: &SearchRequest,
    _search_options: &crate::server::services::search::SearchOptions,
) -> Result<Vec<SearchResultData>> {
    // No-op: ML features not available, return empty results
    Ok(vec![])
}

/// GET /insights/list/topics - List all topics
pub async fn list_topics() -> Result<
    ResponseJson<BaseResponse<ListTopicsResponse>>,
    (axum::http::StatusCode, ResponseJson<BaseResponse<()>>),
> {
    let transaction_id = Uuid::new_v4();

    match insight::get_topics() {
        Ok(topics) => {
            let response = ListTopicsResponse { topics };
            Ok(ResponseJson(BaseResponse::success(
                response,
                transaction_id,
            )))
        }
        Err(e) => {
            let error = ApiError::new("topics_list_failed", &format!("Failed to list topics: {e}"));
            Err((
                axum::http::StatusCode::INTERNAL_SERVER_ERROR,
                ResponseJson(BaseResponse::<()>::error(vec![error], transaction_id)),
            ))
        }
    }
}

/// GET /insights/list/insights - List insights with optional filtering  
pub async fn list_insights() -> Result<
    ResponseJson<BaseResponse<ListInsightsResponse>>,
    (axum::http::StatusCode, ResponseJson<BaseResponse<()>>),
> {
    let transaction_id = Uuid::new_v4();

    // For now, ignore filters and get all insights - we can add filtering later
    match insight::get_insights(None) {
        Ok(insights) => {
            let insight_summaries: Vec<InsightSummary> = insights
                .into_iter()
                .map(|insight| InsightSummary {
                    topic: insight.topic,
                    name: insight.name,
                    overview: insight.overview,
                    pinned: insight.pinned,
                    retrieval_count: insight.retrieval_count,
                    search_hit_count: insight.search_hit_count,
                    last_accessed: insight.last_accessed,
                    created_at: insight.created_at,
                    updated_at: insight.last_updated,
                })
                .collect();

            let response = ListInsightsResponse {
                insights: insight_summaries,
            };
            Ok(ResponseJson(BaseResponse::success(
                response,
                transaction_id,
            )))
        }
        Err(e) => {
            let error = ApiError::new(
                "insights_list_failed",
                &format!("Failed to list insights: {e}"),
            );
            Err((
                axum::http::StatusCode::INTERNAL_SERVER_ERROR,
                ResponseJson(BaseResponse::<()>::error(vec![error], transaction_id)),
            ))
        }
    }
}

/// POST /insights/add - Add a new insight
#[axum::debug_handler]
pub async fn add_insight(
    Extension(context): Extension<RequestContext>,
    Json(request): Json<AddInsightRequest>,
) -> Result<ResponseJson<BaseResponse<()>>, (axum::http::StatusCode, ResponseJson<BaseResponse<()>>)>
{
    let transaction_id = Uuid::new_v4();

    log_insight_addition_start(&context, &request).await;
    let new_insight = create_insight_from_request(request);

    save_insight_with_embedding(&context, &new_insight, transaction_id).await
}

/// Log the start of insight addition operation
async fn log_insight_addition_start(context: &RequestContext, request: &AddInsightRequest) {
    context
        .log_info(
            &format!("Adding insight {}/{}", request.topic, request.name),
            "insights-api",
        )
        .await;
}

/// Create a new insight from the API request
fn create_insight_from_request(request: AddInsightRequest) -> insight::Insight {
    insight::Insight::new(
        request.topic,
        request.name,
        request.overview,
        request.details,
    )
}

/// Save insight and attempt to generate embedding
async fn save_insight_with_embedding(
    context: &RequestContext,
    new_insight: &insight::Insight,
    transaction_id: Uuid,
) -> Result<ResponseJson<BaseResponse<()>>, (axum::http::StatusCode, ResponseJson<BaseResponse<()>>)>
{
    insight::save(new_insight)
        .map_err(|e| create_insight_save_error(context, new_insight, e, transaction_id))?;

    attempt_embedding_generation(context, new_insight).await;

    Ok(ResponseJson(BaseResponse::success((), transaction_id)))
}

/// Attempt to generate and store embedding (non-fatal if fails)
async fn attempt_embedding_generation(context: &RequestContext, insight: &insight::Insight) {
    match generate_and_store_embedding(context, insight).await {
        Ok(_) => {
            log_embedding_success(context, insight).await;
        }
        Err(e) => {
            log_embedding_warning(context, e).await;
        }
    }
}

/// Log successful insight addition with embedding
async fn log_embedding_success(context: &RequestContext, insight: &insight::Insight) {
    context
        .log_success(
            &format!(
                "Successfully added insight {}/{} with embedding",
                insight.topic, insight.name
            ),
            "insights-api",
        )
        .await;
}

/// Log embedding generation warning (non-fatal)
async fn log_embedding_warning(context: &RequestContext, error: anyhow::Error) {
    context
        .log_warn(
            &format!("Insight saved but embedding storage failed: {error}"),
            "insights-api",
        )
        .await;
}

/// Create error response for insight save failure
fn create_insight_save_error(
    context: &RequestContext,
    insight: &insight::Insight,
    error: anyhow::Error,
    transaction_id: Uuid,
) -> (axum::http::StatusCode, ResponseJson<BaseResponse<()>>) {
    // Spawn async logging to avoid blocking the error response
    tokio::spawn({
        let context = context.clone();
        let topic = insight.topic.clone();
        let name = insight.name.clone();
        let error_msg = format!("Failed to add insight {topic}/{name}: {error}");
        async move {
            context.log_error(&error_msg, "insights-api").await;
        }
    });

    let api_error = ApiError::new(
        "insight_add_failed",
        &format!("Failed to add insight: {error}"),
    );
    (
        axum::http::StatusCode::INTERNAL_SERVER_ERROR,
        ResponseJson(BaseResponse::<()>::error(vec![api_error], transaction_id)),
    )
}

/// POST /insights/get - Get a specific insight
pub async fn get_insight(
    Extension(context): Extension<RequestContext>,
    Json(request): Json<GetInsightRequest>,
) -> Result<
    ResponseJson<BaseResponse<GetInsightResponse>>,
    (axum::http::StatusCode, ResponseJson<BaseResponse<()>>),
> {
    let transaction_id = Uuid::new_v4();

    context
        .log_info(
            &format!("Retrieving insight {}/{}", request.topic, request.name),
            "insights-api",
        )
        .await;

    match insight::load(&request.topic, &request.name) {
        Ok(mut insight_data) => {
            if let Err(e) = insight::record_access(&mut insight_data, AccessType::Retrieval) {
                context
                    .log_warn(
                        &format!(
                            "Failed to record access for {}/{}: {e}",
                            request.topic, request.name
                        ),
                        "insights-api",
                    )
                    .await;
            }

            context
                .log_success(
                    &format!(
                        "Successfully retrieved insight {}/{}",
                        request.topic, request.name
                    ),
                    "insights-api",
                )
                .await;

            let insight = InsightData {
                topic: insight_data.topic,
                name: insight_data.name,
                overview: insight_data.overview,
                details: if request.overview_only {
                    String::new()
                } else {
                    insight_data.details
                },
                pinned: insight_data.pinned,
                retrieval_count: insight_data.retrieval_count,
                search_hit_count: insight_data.search_hit_count,
                last_accessed: insight_data.last_accessed,
                embedding_version: insight_data.embedding_version,
                embedding_computed: insight_data.embedding_computed,
            };
            let response = GetInsightResponse { insight };
            Ok(ResponseJson(BaseResponse::success(
                response,
                transaction_id,
            )))
        }
        Err(e) => {
            context
                .log_warn(
                    &format!(
                        "Insight {}/{} not found: {}",
                        request.topic, request.name, e
                    ),
                    "insights-api",
                )
                .await;
            let error = ApiError::new("insight_get_failed", &format!("Failed to get insight: {e}"));
            Err((
                axum::http::StatusCode::NOT_FOUND,
                ResponseJson(BaseResponse::<()>::error(vec![error], transaction_id)),
            ))
        }
    }
}

/// POST /insights/search - Search insights
pub async fn search_insights(
    Extension(context): Extension<RequestContext>,
    Json(request): Json<SearchRequest>,
) -> Result<
    ResponseJson<BaseResponse<SearchResponse>>,
    (axum::http::StatusCode, ResponseJson<BaseResponse<()>>),
> {
    let transaction_id = Uuid::new_v4();

    log_search_start(&context, &request).await;
    let search_options = build_search_options(&request).map_err(|e| {
        let error = ApiError::new("invalid_search_options", &e.to_string());
        (
            axum::http::StatusCode::BAD_REQUEST,
            ResponseJson(BaseResponse::<()>::error(vec![error], transaction_id)),
        )
    })?;

    let mut all_results =
        perform_term_search(&context, &request, &search_options, transaction_id).await?;

    let should_finalize =
        add_embedding_search_results(&context, &request, &search_options, &mut all_results).await;

    if should_finalize {
        Ok(ResponseJson(
            finalize_search_results(
                &context,
                &request,
                &search_options,
                all_results,
                transaction_id,
            )
            .await,
        ))
    } else {
        sort_and_deduplicate_results(&mut all_results, request.sort);
        apply_max_results_limit(&mut all_results, &search_options);
        spawn_search_hit_tracking(&all_results);
        let response_data = SearchResponse {
            count: all_results.len(),
            results: all_results,
        };
        Ok(ResponseJson(BaseResponse::success(
            response_data,
            transaction_id,
        )))
    }
}

/// Log the start of a search operation
async fn log_search_start(context: &RequestContext, request: &SearchRequest) {
    context
        .log_info(
            &format!(
                "Searching insights: terms={:?}, topic={:?}",
                request.terms, request.topic
            ),
            "insights-api",
        )
        .await;
}

/// Build search options from the request
fn build_search_options(
    request: &SearchRequest,
) -> Result<crate::server::services::search::SearchOptions> {
    crate::server::services::search::SearchOptions::from_request(request)
}

/// Perform term-based search and return results
async fn perform_term_search(
    context: &RequestContext,
    request: &SearchRequest,
    search_options: &crate::server::services::search::SearchOptions,
    transaction_id: Uuid,
) -> Result<Vec<SearchResultData>, (axum::http::StatusCode, ResponseJson<BaseResponse<()>>)> {
    let search_results = crate::server::services::search::search(&request.terms, search_options)
        .map_err(|e| {
            let error_response =
                create_search_error_response(&format!("Term search failed: {e}"), transaction_id);
            tokio::spawn({
                let context = context.clone();
                let terms = request.terms.clone();
                let error = format!("Term search failed for {terms:?}: {e}");
                async move {
                    context.log_error(&error, "insights-api").await;
                }
            });
            error_response
        })?;

    context
        .log_info(
            &format!(
                "Term search found {} results for {:?}",
                search_results.len(),
                request.terms
            ),
            "insights-api",
        )
        .await;

    let term_results = convert_search_results_to_api_format(search_results);
    Ok(term_results)
}

/// Convert internal SearchResult to API SearchResultData format
fn convert_search_results_to_api_format(
    search_results: Vec<crate::server::services::search::SearchResult>,
) -> Vec<SearchResultData> {
    search_results
        .into_iter()
        .map(|result| SearchResultData {
            topic: result.topic,
            name: result.name,
            overview: result.overview,
            details: result.details,
            score: result.score,
            created_at: result.created_at,
            updated_at: result.updated_at,
        })
        .collect()
}

/// Add embedding search results if appropriate, returns true if should continue with finalization
async fn add_embedding_search_results(
    context: &RequestContext,
    request: &SearchRequest,
    search_options: &crate::server::services::search::SearchOptions,
    all_results: &mut Vec<SearchResultData>,
) -> bool {
    if should_skip_embedding_search(request) {
        return true;
    }

    match check_embeddings_availability(context, request).await {
        EmbeddingAvailability::Available => {
            execute_embedding_search(context, request, search_options, all_results).await;
            true
        }
        EmbeddingAvailability::Unavailable => false,
        EmbeddingAvailability::Error => true,
    }
}

/// Check if embedding search should be skipped
fn should_skip_embedding_search(request: &SearchRequest) -> bool {
    request.exact || request.semantic
}

/// Execute embedding search and handle results
async fn execute_embedding_search(
    context: &RequestContext,
    request: &SearchRequest,
    search_options: &crate::server::services::search::SearchOptions,
    all_results: &mut Vec<SearchResultData>,
) {
    match perform_vector_search(context, request, search_options).await {
        Ok(embedding_results) => {
            log_embedding_search_success(context, &embedding_results, &request.terms).await;
            all_results.extend(embedding_results);
        }
        Err(_) => {
            log_embedding_search_failure(context, &request.terms).await;
        }
    }
}

/// Log successful embedding search
async fn log_embedding_search_success(
    context: &RequestContext,
    results: &[SearchResultData],
    terms: &[String],
) {
    context
        .log_info(
            &format!(
                "Embedding search found {} results for {:?}",
                results.len(),
                terms
            ),
            "insights-api",
        )
        .await;
}

/// Log embedding search failure
async fn log_embedding_search_failure(context: &RequestContext, terms: &[String]) {
    context
        .log_error(
            &format!("Embedding search failed for {terms:?}"),
            "insights-api",
        )
        .await;
}

/// Check if embeddings are available for search
#[cfg(feature = "ml-features")]
async fn check_embeddings_availability(
    context: &RequestContext,
    request: &SearchRequest,
) -> EmbeddingAvailability {
    match context.vector_db.has_embeddings().await {
        Ok(true) => {
            context
                .log_info(
                    &format!(
                        "Starting embedding search for {:?} (embeddings exist)",
                        request.terms
                    ),
                    "insights-api",
                )
                .await;
            EmbeddingAvailability::Available
        }
        Ok(false) => {
            context
                .log_info(
                    &format!(
                        "Skipping embedding search for {:?} (no embeddings in database)",
                        request.terms
                    ),
                    "insights-api",
                )
                .await;
            EmbeddingAvailability::Unavailable
        }
        Err(e) => {
            context
                .log_error(
                    &format!("Failed to check for embeddings: {e}"),
                    "insights-api",
                )
                .await;
            EmbeddingAvailability::Error
        }
    }
}

/// Check if embeddings are available for search (no-op without ml-features)
#[cfg(not(feature = "ml-features"))]
async fn check_embeddings_availability(
    context: &RequestContext,
    request: &SearchRequest,
) -> EmbeddingAvailability {
    context
        .log_info(
            &format!(
                "Skipping embedding search for {:?} (ML features not available)",
                request.terms
            ),
            "insights-api",
        )
        .await;
    EmbeddingAvailability::Unavailable
}

/// Embedding availability status
#[allow(dead_code)] // Some variants only used with ml-features
enum EmbeddingAvailability {
    Available,
    Unavailable,
    Error,
}

/// Spawn fire-and-forget tasks to record search hit access for each result
fn spawn_search_hit_tracking(results: &[SearchResultData]) {
    for result in results {
        let topic = result.topic.clone();
        let name = result.name.clone();
        tokio::spawn(async move {
            if let Ok(mut insight_data) = insight::load(&topic, &name) {
                let _ = insight::record_access(&mut insight_data, AccessType::SearchHit);
            }
        });
    }
}

/// Sort, deduplicate, and create final response
async fn finalize_search_results(
    context: &RequestContext,
    request: &SearchRequest,
    search_options: &crate::server::services::search::SearchOptions,
    mut all_results: Vec<SearchResultData>,
    transaction_id: Uuid,
) -> BaseResponse<SearchResponse> {
    sort_and_deduplicate_results(&mut all_results, request.sort);
    apply_max_results_limit(&mut all_results, search_options);
    spawn_search_hit_tracking(&all_results);

    context
        .log_success(
            &format!(
                "Search completed: found {} results for {:?}",
                all_results.len(),
                request.terms
            ),
            "insights-api",
        )
        .await;

    let response_data = SearchResponse {
        count: all_results.len(),
        results: all_results,
    };
    BaseResponse::success(response_data, transaction_id)
}

fn apply_max_results_limit(
    results: &mut Vec<SearchResultData>,
    options: &crate::server::services::search::SearchOptions,
) {
    if let Some(limit) = options.max_results {
        results.truncate(limit);
    }
}

fn sort_and_deduplicate_results(
    all_results: &mut Vec<SearchResultData>,
    sort: crate::server::types::SearchSort,
) {
    sort_results_by_relevance(all_results);

    let mut seen = std::collections::HashSet::new();
    all_results.retain(|result| {
        let key = (result.topic.clone(), result.name.clone());
        seen.insert(key)
    });

    sort_results_by_requested_order(all_results, sort);
}

fn sort_results_by_relevance(results: &mut [SearchResultData]) {
    results.sort_by(|a, b| {
        b.score
            .partial_cmp(&a.score)
            .unwrap_or(std::cmp::Ordering::Equal)
            .then_with(|| a.topic.cmp(&b.topic).then_with(|| a.name.cmp(&b.name)))
    });
}

fn sort_results_by_requested_order(
    results: &mut [SearchResultData],
    sort: crate::server::types::SearchSort,
) {
    match sort {
        crate::server::types::SearchSort::Relevance => sort_results_by_relevance(results),
        crate::server::types::SearchSort::Updated => results.sort_by(|a, b| {
            b.updated_at
                .cmp(&a.updated_at)
                .then_with(|| a.topic.cmp(&b.topic).then_with(|| a.name.cmp(&b.name)))
        }),
        crate::server::types::SearchSort::Created => results.sort_by(|a, b| {
            b.created_at
                .cmp(&a.created_at)
                .then_with(|| a.topic.cmp(&b.topic).then_with(|| a.name.cmp(&b.name)))
        }),
        crate::server::types::SearchSort::LeastAccessed => {
            results.sort_by(|a, b| {
                a.score
                    .partial_cmp(&b.score)
                    .unwrap_or(std::cmp::Ordering::Equal)
                    .then_with(|| a.topic.cmp(&b.topic).then_with(|| a.name.cmp(&b.name)))
            });
        }
    }
}

/// Create a standardized error response for search failures
fn create_search_error_response(
    message: &str,
    transaction_id: Uuid,
) -> (axum::http::StatusCode, ResponseJson<BaseResponse<()>>) {
    let error = ApiError::new("search_failed", message);
    (
        axum::http::StatusCode::INTERNAL_SERVER_ERROR,
        ResponseJson(BaseResponse::<()>::error(vec![error], transaction_id)),
    )
}

/// Get the configured initial limit for reranking candidate retrieval
/// Default: 128 candidates
/// Environment: INSIGHTS_RERANK_INITIAL_LIMIT
#[cfg(feature = "ml-features")]
fn get_initial_search_limit() -> usize {
    std::env::var("INSIGHTS_RERANK_INITIAL_LIMIT")
        .ok()
        .and_then(|s| s.parse().ok())
        .unwrap_or(128)
}

/// Get the configured initial threshold for reranking candidate retrieval
/// Default: 0.3 (more permissive than final search)
/// Environment: INSIGHTS_RERANK_INITIAL_THRESHOLD  
#[cfg(feature = "ml-features")]
fn get_initial_search_threshold() -> f32 {
    std::env::var("INSIGHTS_RERANK_INITIAL_THRESHOLD")
        .ok()
        .and_then(|s| s.parse().ok())
        .unwrap_or(0.3)
}

/// Get the configured final limit for reranking results
/// Default: 8 final results after reranking
/// Environment: INSIGHTS_RERANK_FINAL_LIMIT
#[cfg(feature = "ml-features")]
fn get_rerank_limit() -> usize {
    std::env::var("INSIGHTS_RERANK_FINAL_LIMIT")
        .ok()
        .and_then(|s| s.parse().ok())
        .unwrap_or(8)
}
