// file: kb_lib/src/trade_event_materialization.rs

//! Trade-event materialization.
//!
//! This module persists normalized trade events, updates pair metrics and
//! records the corresponding detection observation/signal.

/// Input required to materialize one normalized trade event.
pub(crate) struct TradeEventMaterializationInput<'a> {
    /// Database connection.
    pub(crate) database: &'a crate::Database,
    /// Detection persistence service used for observations and signals.
    pub(crate) persistence: &'a crate::DetectionPersistenceService,
    /// Persisted transaction row.
    pub(crate) transaction: &'a crate::ChainTransactionDto,
    /// Internal transaction id.
    pub(crate) transaction_id: i64,
    /// Decoded DEX event row.
    pub(crate) decoded_event: &'a crate::DexDecodedEventDto,
    /// Internal decoded event id.
    pub(crate) decoded_event_id: i64,
    /// Existing trade event, when the decoded event was already materialized.
    pub(crate) existing_trade_event: std::option::Option<crate::TradeEventDto>,
    /// Persisted pool row.
    pub(crate) pool: &'a crate::PoolDto,
    /// Internal pool id.
    pub(crate) pool_id: i64,
    /// Persisted pair row.
    pub(crate) pair: &'a crate::PairDto,
    /// Internal pair id.
    pub(crate) pair_id: i64,
    /// Trade side.
    pub(crate) trade_side: crate::SwapTradeSide,
    /// Resolved trade amounts.
    pub(crate) amount_resolution: &'a crate::trade_amount_resolution::TradeAmountResolution,
}

/// Persists one normalized trade event and updates pair-level metrics.
pub(crate) async fn materialize_trade_event(
    input: crate::trade_event_materialization::TradeEventMaterializationInput<'_>,
) -> Result<crate::TradeAggregationResult, crate::Error> {
    let base_amount_raw = input.amount_resolution.base_amount_raw.clone();
    let quote_amount_raw = input.amount_resolution.quote_amount_raw.clone();
    let price_quote_per_base = input.amount_resolution.price_quote_per_base;
    let slot_i64 = crate::trade_metric_update::convert_slot_to_i64(input.transaction.slot);
    let created_trade_event = input.existing_trade_event.is_none();
    let trade_event_dto = crate::TradeEventDto::new(
        input.pool.dex_id,
        input.pool_id,
        input.pair_id,
        input.transaction_id,
        input.decoded_event_id,
        input.transaction.signature.clone(),
        slot_i64,
        input.trade_side,
        input.pair.base_token_id,
        input.pair.quote_token_id,
        base_amount_raw.clone(),
        quote_amount_raw.clone(),
        price_quote_per_base,
        crate::ObservationSourceKind::Dex,
        input.transaction.source_endpoint_name.clone(),
        input.decoded_event.payload_json.clone(),
    );
    tracing::debug!(
        event_kind = %input.decoded_event.event_kind,
        pool_account = ?input.decoded_event.pool_account,
        decoded_event_id = ?input.decoded_event.id,
        created_trade_event = created_trade_event,
        "trade aggregation candidate"
    );
    let trade_event_id_result =
        crate::query_trade_events_upsert(input.database, &trade_event_dto).await;
    let trade_event_id = match trade_event_id_result {
        Ok(trade_event_id) => trade_event_id,
        Err(error) => return Err(error),
    };
    let pair_metric_id_result = crate::trade_event_materialization::upsert_pair_metric_for_trade(
        input.database,
        input.pair_id,
        slot_i64,
        input.transaction.signature.clone(),
        input.trade_side,
        base_amount_raw.clone(),
        quote_amount_raw.clone(),
        price_quote_per_base,
        created_trade_event,
    )
    .await;
    let pair_metric_id = match pair_metric_id_result {
        Ok(pair_metric_id) => pair_metric_id,
        Err(error) => return Err(error),
    };
    if created_trade_event {
        let observation_result =
            crate::trade_event_materialization::record_trade_aggregation_observation_and_signal(
                input.persistence,
                input.transaction,
                input.pair_id,
                input.pool_id,
                trade_event_id,
                input.trade_side,
                base_amount_raw,
                quote_amount_raw,
                price_quote_per_base,
            )
            .await;
        if let Err(error) = observation_result {
            return Err(error);
        }
    }
    return Ok(crate::TradeAggregationResult {
        trade_event_id,
        pair_metric_id,
        pair_id: input.pair_id,
        pool_id: input.pool_id,
        created_trade_event,
    });
}

async fn upsert_pair_metric_for_trade(
    database: &crate::Database,
    pair_id: i64,
    slot_i64: std::option::Option<i64>,
    signature: std::string::String,
    trade_side: crate::SwapTradeSide,
    base_amount_raw: std::option::Option<std::string::String>,
    quote_amount_raw: std::option::Option<std::string::String>,
    price_quote_per_base: std::option::Option<f64>,
    created_trade_event: bool,
) -> Result<i64, crate::Error> {
    let pair_metric_result = crate::query_pair_metrics_get_by_pair_id(database, pair_id).await;
    let pair_metric_option = match pair_metric_result {
        Ok(pair_metric_option) => pair_metric_option,
        Err(error) => return Err(error),
    };
    if let Some(existing_metric) = pair_metric_option {
        let existing_metric_id = match existing_metric.id {
            Some(existing_metric_id) => existing_metric_id,
            None => {
                return Err(crate::Error::InvalidState(
                    "pair metric has no internal id".to_string(),
                ));
            },
        };
        if created_trade_event {
            let mut updated_metric = existing_metric.clone();
            crate::trade_metric_update::apply_trade_to_pair_metric(
                &mut updated_metric,
                slot_i64,
                signature,
                trade_side,
                base_amount_raw,
                quote_amount_raw,
                price_quote_per_base,
            );
            let upsert_result = crate::query_pair_metrics_upsert(database, &updated_metric).await;
            if let Err(error) = upsert_result {
                return Err(error);
            }
        }
        return Ok(existing_metric_id);
    }
    let mut new_metric = crate::PairMetricDto::new(pair_id);
    crate::trade_metric_update::apply_trade_to_pair_metric(
        &mut new_metric,
        slot_i64,
        signature,
        trade_side,
        base_amount_raw,
        quote_amount_raw,
        price_quote_per_base,
    );
    let upsert_result = crate::query_pair_metrics_upsert(database, &new_metric).await;
    match upsert_result {
        Ok(pair_metric_id) => return Ok(pair_metric_id),
        Err(error) => return Err(error),
    }
}

async fn record_trade_aggregation_observation_and_signal(
    persistence: &crate::DetectionPersistenceService,
    transaction: &crate::ChainTransactionDto,
    pair_id: i64,
    pool_id: i64,
    trade_event_id: i64,
    trade_side: crate::SwapTradeSide,
    base_amount_raw: std::option::Option<std::string::String>,
    quote_amount_raw: std::option::Option<std::string::String>,
    price_quote_per_base: std::option::Option<f64>,
) -> Result<(), crate::Error> {
    let payload = serde_json::json!({
        "pairId": pair_id,
        "poolId": pool_id,
        "tradeEventId": trade_event_id,
        "tradeSide": format!("{:?}", trade_side),
        "baseAmountRaw": base_amount_raw,
        "quoteAmountRaw": quote_amount_raw,
        "priceQuotePerBase": price_quote_per_base,
        "transactionSignature": transaction.signature
    });
    let observation_result = persistence
        .record_observation(&crate::DetectionObservationInput::new(
            "dex.trade_aggregation".to_string(),
            crate::ObservationSourceKind::Dex,
            transaction.source_endpoint_name.clone(),
            transaction.signature.clone(),
            transaction.slot,
            payload.clone(),
        ))
        .await;
    let observation_id = match observation_result {
        Ok(observation_id) => observation_id,
        Err(error) => return Err(error),
    };
    let signal_result = persistence
        .record_signal(&crate::DetectionSignalInput::new(
            "signal.dex.trade_aggregation.recorded".to_string(),
            crate::AnalysisSignalSeverity::Low,
            transaction.signature.clone(),
            Some(observation_id),
            None,
            payload,
        ))
        .await;
    if let Err(error) = signal_result {
        return Err(error);
    }
    return Ok(());
}
