//! Handlers for ConceptPath and Alias operations.
//!
//! Provides CRUD operations for concept aliases and path information.

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

use crate::{
    dto::{
        AliasMapping, AliasOriginDto, AliasResponse, AliasSuggestion, ConceptPathInfo,
        CreateAliasRequest, DeleteAliasRequest, DeleteAliasResponse, ErrorResponse,
        ListAliasesParams, ListAliasesResponse, ResolveAliasParams, ResolveAliasResponse,
        SuggestAliasesResponse,
    },
    error::{ApiError, Result},
    hex,
    state::AppState,
};

use stemedb_core::types::{AliasOrigin, ConceptAlias, ConceptPath};
use stemedb_storage::AliasStore;

/// Create a new concept alias.
///
/// Creates a bidirectional mapping between an alias path and its canonical form.
/// This enables cross-scheme entity resolution in queries.
///
/// # Example
///
/// ```json
/// {
///   "alias_path": "code://rust/auth/jwt/aud",
///   "canonical_path": "rfc://7519/jwt/audience_validation",
///   "agent_id": "deadbeef..."
/// }
/// ```
#[utoipa::path(
    post,
    path = "/v1/concepts/alias",
    request_body = CreateAliasRequest,
    responses(
        (status = 201, description = "Alias created", body = AliasResponse),
        (status = 400, description = "Invalid request", body = ErrorResponse),
        (status = 500, description = "Internal error", body = ErrorResponse)
    ),
    tag = "concepts"
)]
#[instrument(skip(state), fields(alias = %req.alias_path, canonical = %req.canonical_path))]
pub async fn create_alias(
    State(state): State<AppState>,
    Json(req): Json<CreateAliasRequest>,
) -> Result<(StatusCode, Json<AliasResponse>)> {
    // Parse paths
    let alias_path = ConceptPath::parse(&req.alias_path)
        .map_err(|e| ApiError::InvalidRequest(format!("Invalid alias_path: {}", e)))?;

    let canonical_path = ConceptPath::parse(&req.canonical_path)
        .map_err(|e| ApiError::InvalidRequest(format!("Invalid canonical_path: {}", e)))?;

    // Decode agent_id
    let agent_id = hex::decode_hash_32(&req.agent_id)?;

    // Get current timestamp
    let created_at = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map_err(|e| ApiError::Serialization(format!("Failed to get timestamp: {}", e)))?
        .as_secs();

    // Convert origin
    let origin = match req.origin.unwrap_or_default() {
        AliasOriginDto::Manual => AliasOrigin::Manual,
        AliasOriginDto::Suggested => AliasOrigin::Suggested,
        AliasOriginDto::Merged => AliasOrigin::Merged,
    };

    // Create the alias
    let alias = ConceptAlias::new(alias_path, canonical_path, agent_id, created_at, origin);

    // Store the alias
    state.alias_store.set_alias(&alias).await?;

    // Return response
    let response = AliasResponse {
        alias_path: req.alias_path,
        canonical_path: req.canonical_path,
        created_at,
        origin: req.origin.unwrap_or_default(),
    };

    Ok((StatusCode::CREATED, Json(response)))
}

/// Resolve a path to its canonical and/or all related paths.
///
/// If `transitive=true`, returns all paths reachable through the alias graph
/// (including cycles, which are handled safely).
#[utoipa::path(
    get,
    path = "/v1/concepts/resolve",
    params(
        ("path" = String, Query, description = "Path to resolve"),
        ("transitive" = Option<bool>, Query, description = "Resolve transitively (default: false)")
    ),
    responses(
        (status = 200, description = "Resolved paths", body = ResolveAliasResponse),
        (status = 400, description = "Invalid request", body = ErrorResponse),
        (status = 500, description = "Internal error", body = ErrorResponse)
    ),
    tag = "concepts"
)]
#[instrument(skip(state), fields(path = %params.path, transitive = %params.transitive))]
pub async fn resolve_alias(
    State(state): State<AppState>,
    Query(params): Query<ResolveAliasParams>,
) -> Result<Json<ResolveAliasResponse>> {
    let resolved_paths = if params.transitive {
        // Transitive resolution
        state.alias_store.resolve_all(&params.path).await?
    } else {
        // Direct resolution only
        let mut paths = vec![params.path.clone()];
        if let Some(canonical) = state.alias_store.get_canonical(&params.path).await? {
            paths.push(canonical.to_wire_format());
        }
        paths
    };

    Ok(Json(ResolveAliasResponse { input_path: params.path, resolved_paths }))
}

/// Delete a concept alias.
///
/// Removes the alias and updates the reverse index.
#[utoipa::path(
    delete,
    path = "/v1/concepts/alias",
    request_body = DeleteAliasRequest,
    responses(
        (status = 200, description = "Alias deleted", body = DeleteAliasResponse),
        (status = 400, description = "Invalid request", body = ErrorResponse),
        (status = 500, description = "Internal error", body = ErrorResponse)
    ),
    tag = "concepts"
)]
#[instrument(skip(state), fields(alias = %req.alias_path))]
pub async fn delete_alias(
    State(state): State<AppState>,
    Json(req): Json<DeleteAliasRequest>,
) -> Result<Json<DeleteAliasResponse>> {
    let deleted = state.alias_store.delete_alias(&req.alias_path).await?;

    Ok(Json(DeleteAliasResponse { deleted, alias_path: req.alias_path }))
}

/// List all aliases, optionally filtered by canonical path.
#[utoipa::path(
    get,
    path = "/v1/concepts/aliases",
    params(
        ("canonical_path" = Option<String>, Query, description = "Filter by canonical path"),
        ("limit" = Option<usize>, Query, description = "Max results (default: 100)")
    ),
    responses(
        (status = 200, description = "List of aliases", body = ListAliasesResponse),
        (status = 500, description = "Internal error", body = ErrorResponse)
    ),
    tag = "concepts"
)]
#[instrument(skip(state), fields(canonical = ?params.canonical_path, limit = %params.limit))]
pub async fn list_aliases(
    State(state): State<AppState>,
    Query(params): Query<ListAliasesParams>,
) -> Result<Json<ListAliasesResponse>> {
    let aliases: Vec<AliasMapping> = if let Some(canonical) = &params.canonical_path {
        // Get aliases for a specific canonical
        let alias_paths = state.alias_store.get_aliases(canonical).await?;
        alias_paths
            .into_iter()
            .map(|p| AliasMapping {
                alias_path: p.to_wire_format(),
                canonical_path: canonical.clone(),
            })
            .collect()
    } else {
        // List all aliases
        let all = state.alias_store.list_all_aliases().await?;
        all.into_iter()
            .take(params.limit)
            .map(|(alias, canonical)| AliasMapping { alias_path: alias, canonical_path: canonical })
            .collect()
    };

    let total = aliases.len();
    Ok(Json(ListAliasesResponse { aliases, total }))
}

/// Suggest aliases for a path based on existing subjects.
///
/// Finds existing subjects with similar leaf names across different schemes.
#[utoipa::path(
    get,
    path = "/v1/concepts/suggest",
    params(
        ("path" = String, Query, description = "Path to get suggestions for")
    ),
    responses(
        (status = 200, description = "Alias suggestions", body = SuggestAliasesResponse),
        (status = 400, description = "Invalid request", body = ErrorResponse),
        (status = 500, description = "Internal error", body = ErrorResponse)
    ),
    tag = "concepts"
)]
#[instrument(skip(state), fields(path = %path))]
pub async fn suggest_aliases(
    State(state): State<AppState>,
    Query(path): Query<String>,
) -> Result<Json<SuggestAliasesResponse>> {
    // Get existing subjects from the index
    // For now, we'll get subjects from the alias store's list
    let existing_aliases = state.alias_store.list_all_aliases().await?;
    let existing_subjects: Vec<String> =
        existing_aliases.into_iter().flat_map(|(a, c)| vec![a, c]).collect();

    let suggestions = state.alias_store.suggest_aliases(&path, &existing_subjects).await?;

    let suggestion_dtos: Vec<AliasSuggestion> = suggestions
        .into_iter()
        .map(|(suggested, reason)| AliasSuggestion { suggested_canonical: suggested, reason })
        .collect();

    Ok(Json(SuggestAliasesResponse { input_path: path, suggestions: suggestion_dtos }))
}

/// Parse a path and return ConceptPath information.
///
/// Returns scheme, segments, leaf, and inferred source class.
#[utoipa::path(
    get,
    path = "/v1/concepts/parse",
    params(
        ("path" = String, Query, description = "Path to parse")
    ),
    responses(
        (status = 200, description = "Parsed concept path", body = ConceptPathInfo),
        (status = 400, description = "Invalid path", body = ErrorResponse)
    ),
    tag = "concepts"
)]
#[instrument(fields(path = %path))]
pub async fn parse_concept_path(Query(path): Query<String>) -> Result<Json<ConceptPathInfo>> {
    let concept_path = ConceptPath::parse(&path)
        .map_err(|e| ApiError::InvalidRequest(format!("Invalid path: {}", e)))?;

    let source_class = concept_path.default_source_class();
    let source_class_str = format!("{:?}", source_class);

    Ok(Json(ConceptPathInfo {
        path,
        scheme: concept_path.scheme.clone(),
        segments: concept_path.segments.clone(),
        leaf: concept_path.leaf().to_string(),
        inferred_source_class: source_class_str,
    }))
}
