//! Observation reporting and pattern query handlers for hosted mode and community corpus.

use axum::{extract::State, http::StatusCode, Json};
use stemedb_storage::KVStore;
use tracing::instrument;

use crate::{
    dto::aphoria::{
        CommunityValueDto, GetPatternsRequest, GetPatternsResponse, PatternDto,
        PushCommunityObservationsRequest, PushCommunityObservationsResponse,
        PushObservationsRequest, PushObservationsResponse,
    },
    error::{ApiError, Result},
    state::AppState,
};

use super::super::aphoria_helpers::{compute_assertion_hash, observation_dto_to_assertion};

/// Push observations from an Aphoria client (hosted mode).
///
/// This endpoint receives observations from teams running Aphoria in hosted
/// mode, enabling pattern aggregation across all projects.
#[utoipa::path(
    post,
    path = "/v1/aphoria/observations",
    request_body = PushObservationsRequest,
    responses(
        (status = 201, description = "Observations pushed successfully", body = PushObservationsResponse),
        (status = 400, description = "Invalid request", body = crate::dto::ErrorResponse),
        (status = 500, description = "Internal server error", body = crate::dto::ErrorResponse),
    ),
    tag = "aphoria"
)]
#[instrument(skip_all, fields(project_id = %req.project_id, count = req.observations.len()))]
pub async fn push_observations(
    State(state): State<AppState>,
    Json(req): Json<PushObservationsRequest>,
) -> Result<(StatusCode, Json<PushObservationsResponse>)> {
    use std::time::{SystemTime, UNIX_EPOCH};

    let mut accepted = 0;
    let mut deduplicated = 0;
    let mut hashes = Vec::new();

    let now = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);

    for obs in &req.observations {
        // Convert DTO to Assertion
        let assertion = match observation_dto_to_assertion(
            obs,
            &req.project_id,
            req.team_id.as_deref(),
            now,
        ) {
            Ok(a) => a,
            Err(e) => {
                tracing::warn!(subject = %obs.subject, error = %e, "Skipping invalid observation");
                continue;
            }
        };

        // Compute assertion hash
        let hash = compute_assertion_hash(&assertion);
        let hash_hex = hex::encode(hash);

        // Check if already exists (by subject + predicate)
        let subject_key = format!("subject:{}", assertion.subject);
        let exists =
            state.store.get(subject_key.as_bytes()).await.map_err(|e| {
                ApiError::Internal(format!("Storage error checking existence: {}", e))
            })?;

        if exists.is_some() {
            // For simplicity, treat existing subject as deduplicated
            // A more sophisticated impl would check exact hash
            deduplicated += 1;
        } else {
            // Store the assertion
            // Serialize using stemedb-core's standard serialization
            let serialized = stemedb_core::serde::serialize(&assertion)
                .map_err(|e| ApiError::Internal(format!("Failed to serialize: {}", e)))?;

            // Store by hash
            let hash_key = format!("assertion:{}", hash_hex);
            state
                .store
                .put(hash_key.as_bytes(), &serialized)
                .await
                .map_err(|e| ApiError::Internal(format!("Failed to store assertion: {}", e)))?;

            // Also store by subject for conflict detection
            state
                .store
                .put(subject_key.as_bytes(), &serialized)
                .await
                .map_err(|e| ApiError::Internal(format!("Failed to store subject index: {}", e)))?;

            accepted += 1;
            hashes.push(hash_hex);
        }
    }

    tracing::info!(
        project_id = %req.project_id,
        accepted,
        deduplicated,
        "Processed observations from hosted client"
    );

    Ok((StatusCode::CREATED, Json(PushObservationsResponse { accepted, deduplicated, hashes })))
}

/// Push anonymized observations to the community corpus.
///
/// When projects opt-in to community sharing, their anonymized scan observations
/// are aggregated here to establish community consensus on patterns.
///
/// # Privacy Guarantees
///
/// - Project names are wildcarded in subject paths
/// - No file paths, line numbers, or source code is included
/// - Only hashed project identifiers are received (not actual names)
/// - Timestamps are rounded to the hour for k-anonymity
#[utoipa::path(
    post,
    path = "/v1/aphoria/community/observations",
    request_body = PushCommunityObservationsRequest,
    responses(
        (status = 201, description = "Observations recorded successfully", body = PushCommunityObservationsResponse),
        (status = 400, description = "Invalid request", body = crate::dto::ErrorResponse),
        (status = 500, description = "Internal server error", body = crate::dto::ErrorResponse),
    ),
    tag = "aphoria"
)]
#[instrument(skip_all, fields(project_hash = %req.project_hash, count = req.observations.len()))]
pub async fn push_community_observations(
    State(state): State<AppState>,
    Json(req): Json<PushCommunityObservationsRequest>,
) -> Result<(StatusCode, Json<PushCommunityObservationsResponse>)> {
    use stemedb_storage::{GenericPatternAggregateStore, PatternAggregateStore};

    let pattern_store = GenericPatternAggregateStore::new(state.store.clone());

    let mut recorded = 0;
    let mut new_patterns = 0;
    let mut updated_patterns = 0;

    for obs in &req.observations {
        // Convert value to display string
        let value_display = match &obs.object {
            CommunityValueDto::Boolean(b) => b.to_string(),
            CommunityValueDto::Number(n) => n.to_string(),
            CommunityValueDto::Text(s) => s.clone(),
        };

        // Use the anon_hash as the value_hash for storage
        let value_hash = &obs.anon_hash;

        // Record the observation
        let agg = pattern_store
            .record_observation(
                &obs.subject,
                &obs.predicate,
                value_hash,
                &value_display,
                &req.project_hash,
                obs.timestamp_hour,
            )
            .await
            .map_err(|e| ApiError::Internal(format!("Failed to record observation: {}", e)))?;

        recorded += 1;

        // Check if this is a new pattern (observation_count == 1)
        if agg.observation_count == 1 {
            new_patterns += 1;
        } else {
            updated_patterns += 1;
        }
    }

    tracing::info!(
        project_hash = %req.project_hash,
        recorded,
        new_patterns,
        updated_patterns,
        "Processed community observations"
    );

    Ok((
        StatusCode::CREATED,
        Json(PushCommunityObservationsResponse { recorded, new_patterns, updated_patterns }),
    ))
}

/// Get popular patterns from the community corpus.
///
/// Returns patterns that have been reported by multiple projects, ordered
/// by project count descending. These patterns represent community consensus
/// on what configurations are commonly used.
#[utoipa::path(
    get,
    path = "/v1/aphoria/patterns",
    params(
        ("min_projects" = u64, Query, description = "Minimum number of projects (default: 3)"),
        ("limit" = usize, Query, description = "Maximum patterns to return (default: 100)"),
        ("subject_prefix" = Option<String>, Query, description = "Filter by subject prefix"),
    ),
    responses(
        (status = 200, description = "Patterns retrieved successfully", body = GetPatternsResponse),
        (status = 400, description = "Invalid request", body = crate::dto::ErrorResponse),
        (status = 500, description = "Internal server error", body = crate::dto::ErrorResponse),
    ),
    tag = "aphoria"
)]
#[instrument(skip_all, fields(min_projects = params.min_projects, limit = params.limit))]
pub async fn get_patterns(
    State(state): State<AppState>,
    axum::extract::Query(params): axum::extract::Query<GetPatternsRequest>,
) -> Result<Json<GetPatternsResponse>> {
    use stemedb_storage::{GenericPatternAggregateStore, PatternAggregateStore};

    let pattern_store = GenericPatternAggregateStore::new(state.store.clone());

    let aggregates = if let Some(prefix) = &params.subject_prefix {
        pattern_store
            .get_patterns_for_subject_prefix(prefix, params.min_projects, params.limit)
            .await
            .map_err(|e| ApiError::Internal(format!("Failed to get patterns: {}", e)))?
    } else {
        pattern_store
            .get_popular_patterns(params.min_projects, params.limit)
            .await
            .map_err(|e| ApiError::Internal(format!("Failed to get patterns: {}", e)))?
    };

    let total_matching = aggregates.len();

    let patterns: Vec<PatternDto> = aggregates
        .into_iter()
        .map(|agg| PatternDto {
            subject: agg.subject,
            predicate: agg.predicate,
            value: agg.value_display,
            project_count: agg.project_count,
            observation_count: agg.observation_count,
            first_seen: agg.first_seen,
            last_seen: agg.last_seen,
        })
        .collect();

    Ok(Json(GetPatternsResponse { patterns, total_matching }))
}
