// file: kb_lib/src/db/queries/fee_event_amount.rs

//! Queries for `k_sol_fee_event_amounts`.

/// Deletes fee amount legs for one parent fee event.
pub async fn query_fee_event_amounts_delete_by_fee_event_id(
    database: &crate::Database,
    fee_event_id: i64,
) -> Result<u64, crate::Error> {
    match database.connection() {
        crate::DatabaseConnection::Sqlite(pool) => {
            let delete_result = sqlx::query(
                r#"
DELETE FROM k_sol_fee_event_amounts
WHERE fee_event_id = ?
                "#,
            )
            .bind(fee_event_id)
            .execute(pool)
            .await;
            match delete_result {
                Ok(result) => return Ok(result.rows_affected()),
                Err(error) => {
                    return Err(crate::Error::Db(format!(
                        "cannot delete k_sol_fee_event_amounts for fee_event_id '{}' on sqlite: {}",
                        fee_event_id, error
                    )));
                },
            }
        },
    }
}

/// Replaces all fee amount legs for one parent fee event.
pub async fn query_fee_event_amounts_replace_for_fee_event(
    database: &crate::Database,
    fee_event_id: i64,
    dtos: &[crate::FeeEventAmountDto],
) -> Result<u64, crate::Error> {
    let delete_result =
        query_fee_event_amounts_delete_by_fee_event_id(database, fee_event_id).await;
    if let Err(error) = delete_result {
        return Err(error);
    }
    let mut inserted: u64 = 0;
    match database.connection() {
        crate::DatabaseConnection::Sqlite(pool) => {
            for dto in dtos {
                let leg_index_i64 = i64::from(dto.leg_index);
                let insert_result = sqlx::query(
                    r#"
INSERT INTO k_sol_fee_event_amounts (
    fee_event_id,
    transaction_id,
    decoded_event_id,
    leg_index,
    fee_component_kind,
    token_mint,
    amount_raw,
    source_account,
    destination_account,
    amount_source,
    payload_json,
    created_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                    "#,
                )
                .bind(dto.fee_event_id)
                .bind(dto.transaction_id)
                .bind(dto.decoded_event_id)
                .bind(leg_index_i64)
                .bind(dto.fee_component_kind.clone())
                .bind(dto.token_mint.clone())
                .bind(dto.amount_raw.clone())
                .bind(dto.source_account.clone())
                .bind(dto.destination_account.clone())
                .bind(dto.amount_source.clone())
                .bind(dto.payload_json.clone())
                .bind(dto.created_at.to_rfc3339())
                .execute(pool)
                .await;
                match insert_result {
                    Ok(_) => inserted += 1,
                    Err(error) => {
                        return Err(crate::Error::Db(format!(
                            "cannot insert k_sol_fee_event_amounts leg '{}' for fee_event_id '{}' on sqlite: {}",
                            dto.leg_index, fee_event_id, error
                        )));
                    },
                }
            }
        },
    }
    return Ok(inserted);
}

/// Lists fee amount legs for one parent fee event.
pub async fn query_fee_event_amounts_list_by_fee_event_id(
    database: &crate::Database,
    fee_event_id: i64,
) -> Result<std::vec::Vec<crate::FeeEventAmountDto>, crate::Error> {
    match database.connection() {
        crate::DatabaseConnection::Sqlite(pool) => {
            let query_result = sqlx::query_as::<sqlx::Sqlite, crate::FeeEventAmountEntity>(
                r#"
SELECT
    id,
    fee_event_id,
    transaction_id,
    decoded_event_id,
    leg_index,
    fee_component_kind,
    token_mint,
    amount_raw,
    source_account,
    destination_account,
    amount_source,
    payload_json,
    created_at
FROM k_sol_fee_event_amounts
WHERE fee_event_id = ?
ORDER BY leg_index ASC
                "#,
            )
            .bind(fee_event_id)
            .fetch_all(pool)
            .await;
            let entities = match query_result {
                Ok(entities) => entities,
                Err(error) => {
                    return Err(crate::Error::Db(format!(
                        "cannot list k_sol_fee_event_amounts for fee_event_id '{}' on sqlite: {}",
                        fee_event_id, error
                    )));
                },
            };
            let mut dtos = std::vec::Vec::new();
            for entity in entities {
                let dto_result = crate::FeeEventAmountDto::try_from(entity);
                match dto_result {
                    Ok(dto) => dtos.push(dto),
                    Err(error) => return Err(error),
                }
            }
            return Ok(dtos);
        },
    }
}

/// Inserts or updates a fee parent and atomically replaces its amount legs.
pub async fn query_fee_events_upsert_with_amount_legs(
    database: &crate::Database,
    fee_event: &crate::FeeEventDto,
    amount_legs: &[crate::FeeEventAmountDto],
) -> Result<i64, crate::Error> {
    let fee_event_id_result = crate::query_fee_events_upsert(database, fee_event).await;
    let fee_event_id = match fee_event_id_result {
        Ok(fee_event_id) => fee_event_id,
        Err(error) => return Err(error),
    };
    let mut normalized_legs = std::vec::Vec::new();
    for amount_leg in amount_legs {
        let mut normalized_leg = amount_leg.clone();
        normalized_leg.fee_event_id = fee_event_id;
        normalized_leg.transaction_id = fee_event.transaction_id;
        normalized_leg.decoded_event_id = fee_event.decoded_event_id;
        normalized_legs.push(normalized_leg);
    }
    let replace_result = query_fee_event_amounts_replace_for_fee_event(
        database,
        fee_event_id,
        normalized_legs.as_slice(),
    )
    .await;
    if let Err(error) = replace_result {
        return Err(error);
    }
    return Ok(fee_event_id);
}

/// Backfills one amount leg for existing fee parents that expose one scalar amount.
pub async fn query_fee_event_amounts_backfill_single_leg_from_fee_events(
    database: &crate::Database,
) -> Result<u64, crate::Error> {
    let entities = match database.connection() {
        crate::DatabaseConnection::Sqlite(pool) => {
            let query_result = sqlx::query_as::<sqlx::Sqlite, crate::FeeEventEntity>(
                r#"
SELECT
    f.id,
    f.transaction_id,
    f.decoded_event_id,
    f.dex_id,
    f.pool_id,
    f.pair_id,
    f.signature,
    f.slot,
    f.protocol_name,
    f.program_id,
    f.event_kind,
    f.pool_account,
    f.actor_wallet,
    f.fee_token_mint,
    f.fee_amount_raw,
    f.payload_json,
    f.executed_at,
    f.created_at
FROM k_sol_fee_events f
WHERE f.fee_token_mint IS NOT NULL
  AND TRIM(f.fee_token_mint) <> ''
  AND f.fee_amount_raw IS NOT NULL
  AND TRIM(f.fee_amount_raw) <> ''
  AND NOT EXISTS (
      SELECT 1
      FROM k_sol_fee_event_amounts a
      WHERE a.fee_event_id = f.id
      LIMIT 1
  )
ORDER BY f.id ASC
                "#,
            )
            .fetch_all(pool)
            .await;
            match query_result {
                Ok(entities) => entities,
                Err(error) => {
                    return Err(crate::Error::Db(format!(
                        "cannot list fee events for single-leg amount backfill on sqlite: {}",
                        error
                    )));
                },
            }
        },
    };
    let mut backfilled: u64 = 0;
    for entity in entities {
        let dto_result = crate::FeeEventDto::try_from(entity);
        let dto = match dto_result {
            Ok(dto) => dto,
            Err(error) => return Err(error),
        };
        let fee_event_id = match dto.id {
            Some(id) => id,
            None => {
                return Err(crate::Error::InvalidState(
                    "fee event backfill row does not contain an id".to_string(),
                ));
            },
        };
        let token_mint = match dto.fee_token_mint.as_ref() {
            Some(token_mint) => token_mint.clone(),
            None => continue,
        };
        let amount_raw = match dto.fee_amount_raw.as_ref() {
            Some(amount_raw) => amount_raw.clone(),
            None => continue,
        };
        let payload_json = single_fee_event_amount_backfill_payload_json(
            &dto,
            token_mint.as_str(),
            amount_raw.as_str(),
        );
        let amount_leg = crate::FeeEventAmountDto::new(
            fee_event_id,
            dto.transaction_id,
            dto.decoded_event_id,
            0,
            dto.event_kind.clone(),
            token_mint,
            amount_raw,
            None,
            None,
            "parent_fee_event_backfill".to_string(),
            payload_json,
        );
        let replace_result =
            query_fee_event_amounts_replace_for_fee_event(database, fee_event_id, &[amount_leg])
                .await;
        match replace_result {
            Ok(inserted) => backfilled += inserted,
            Err(error) => return Err(error),
        }
    }
    return Ok(backfilled);
}

fn single_fee_event_amount_backfill_payload_json(
    dto: &crate::FeeEventDto,
    token_mint: &str,
    amount_raw: &str,
) -> std::string::String {
    let mut object = serde_json::Map::new();
    object.insert(
        "decodedEventKind".to_string(),
        serde_json::Value::String(dto.event_kind.clone()),
    );
    object.insert("protocolName".to_string(), serde_json::Value::String(dto.protocol_name.clone()));
    object.insert(
        "legIndex".to_string(),
        serde_json::Value::Number(serde_json::Number::from(0_u64)),
    );
    object.insert("tokenMint".to_string(), serde_json::Value::String(token_mint.to_string()));
    object.insert("amountRaw".to_string(), serde_json::Value::String(amount_raw.to_string()));
    object.insert(
        "amountSource".to_string(),
        serde_json::Value::String("parent_fee_event_backfill".to_string()),
    );
    return serde_json::Value::Object(object).to_string();
}

#[cfg(test)]
mod tests {
    async fn make_database() -> crate::Database {
        let tempdir_result = tempfile::tempdir();
        let tempdir = match tempdir_result {
            Ok(tempdir) => tempdir,
            Err(error) => panic!("tempdir must succeed: {}", error),
        };
        let database_path = tempdir.path().join("fee_event_amount.sqlite3");
        let config = crate::DatabaseConfig {
            enabled: true,
            backend: crate::DatabaseBackend::Sqlite,
            sqlite: crate::SqliteDatabaseConfig {
                path: database_path.to_string_lossy().to_string(),
                create_if_missing: true,
                busy_timeout_ms: 5000,
                max_connections: 1,
                auto_initialize_schema: true,
                use_wal: true,
            },
        };
        let database_result = crate::Database::connect_and_initialize(&config).await;
        match database_result {
            Ok(database) => return database,
            Err(error) => panic!("database init must succeed: {}", error),
        }
    }

    fn make_leg(
        fee_event_id: i64,
        leg_index: u32,
        token_mint: &str,
        amount_raw: &str,
    ) -> crate::FeeEventAmountDto {
        return crate::FeeEventAmountDto::new(
            fee_event_id,
            7001,
            Some(8001),
            leg_index,
            "trading_fee".to_string(),
            token_mint.to_string(),
            amount_raw.to_string(),
            Some(format!("Source{}", leg_index)),
            Some(format!("Destination{}", leg_index)),
            "inner_spl_transfer".to_string(),
            serde_json::json!({
                "test": true,
                "legIndex": leg_index
            })
            .to_string(),
        );
    }

    #[tokio::test]
    async fn fee_event_amounts_replace_roundtrip_supports_multiple_legs() {
        let database = make_database().await;
        let fee_event_id = 501;
        let legs = vec![
            make_leg(fee_event_id, 0, crate::WSOL_MINT_ID, "100"),
            make_leg(fee_event_id, 1, "TokenMint111", "200"),
        ];
        let replace_result =
            crate::query_fee_event_amounts_replace_for_fee_event(&database, fee_event_id, &legs)
                .await;
        let inserted = match replace_result {
            Ok(inserted) => inserted,
            Err(error) => panic!("replace must succeed: {}", error),
        };
        assert_eq!(inserted, 2);

        let list_result =
            crate::query_fee_event_amounts_list_by_fee_event_id(&database, fee_event_id).await;
        let listed = match list_result {
            Ok(listed) => listed,
            Err(error) => panic!("list must succeed: {}", error),
        };
        assert_eq!(listed.len(), 2);
        assert_eq!(listed[0].leg_index, 0);
        assert_eq!(listed[0].token_mint, crate::WSOL_MINT_ID);
        assert_eq!(listed[0].amount_raw, "100");
        assert_eq!(listed[1].leg_index, 1);
        assert_eq!(listed[1].token_mint, "TokenMint111");
        assert_eq!(listed[1].amount_raw, "200");
    }

    #[tokio::test]
    async fn fee_event_amounts_replace_deletes_stale_legs() {
        let database = make_database().await;
        let fee_event_id = 502;
        let initial_legs = vec![
            make_leg(fee_event_id, 0, crate::WSOL_MINT_ID, "100"),
            make_leg(fee_event_id, 1, "TokenMint222", "200"),
        ];
        let initial_result = crate::query_fee_event_amounts_replace_for_fee_event(
            &database,
            fee_event_id,
            &initial_legs,
        )
        .await;
        if let Err(error) = initial_result {
            panic!("initial replace must succeed: {}", error);
        }

        let replacement_legs = vec![make_leg(fee_event_id, 0, "TokenMint333", "300")];
        let replacement_result = crate::query_fee_event_amounts_replace_for_fee_event(
            &database,
            fee_event_id,
            &replacement_legs,
        )
        .await;
        let inserted = match replacement_result {
            Ok(inserted) => inserted,
            Err(error) => panic!("replacement must succeed: {}", error),
        };
        assert_eq!(inserted, 1);

        let list_result =
            crate::query_fee_event_amounts_list_by_fee_event_id(&database, fee_event_id).await;
        let listed = match list_result {
            Ok(listed) => listed,
            Err(error) => panic!("list must succeed: {}", error),
        };
        assert_eq!(listed.len(), 1);
        assert_eq!(listed[0].leg_index, 0);
        assert_eq!(listed[0].token_mint, "TokenMint333");
        assert_eq!(listed[0].amount_raw, "300");
    }

    fn make_fee_event(
        transaction_id: i64,
        decoded_event_id: i64,
        signature: &str,
        fee_token_mint: std::option::Option<&str>,
        fee_amount_raw: std::option::Option<&str>,
    ) -> crate::FeeEventDto {
        return crate::FeeEventDto::new(
            transaction_id,
            Some(decoded_event_id),
            None,
            None,
            None,
            signature.to_string(),
            Some(1234),
            "test_protocol".to_string(),
            "TestProgram111".to_string(),
            "test_protocol.claim_fee".to_string(),
            None,
            None,
            fee_token_mint.map(|value| return value.to_string()),
            fee_amount_raw.map(|value| return value.to_string()),
            serde_json::json!({"test": true}).to_string(),
        );
    }

    async fn insert_legacy_fee_event_parent(
        database: &crate::Database,
        fee_event: &crate::FeeEventDto,
    ) -> i64 {
        let slot_i64 = match fee_event.slot {
            Some(slot) => {
                let slot_result = i64::try_from(slot);
                match slot_result {
                    Ok(slot_i64) => Some(slot_i64),
                    Err(error) => panic!("slot conversion must succeed: {}", error),
                }
            },
            None => None,
        };
        match database.connection() {
            crate::DatabaseConnection::Sqlite(pool) => {
                let insert_result = sqlx::query(
                    r#"
INSERT INTO k_sol_fee_events (
    transaction_id,
    decoded_event_id,
    dex_id,
    pool_id,
    pair_id,
    signature,
    slot,
    protocol_name,
    program_id,
    event_kind,
    pool_account,
    actor_wallet,
    fee_token_mint,
    fee_amount_raw,
    payload_json,
    executed_at,
    created_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                    "#,
                )
                .bind(fee_event.transaction_id)
                .bind(fee_event.decoded_event_id)
                .bind(fee_event.dex_id)
                .bind(fee_event.pool_id)
                .bind(fee_event.pair_id)
                .bind(fee_event.signature.clone())
                .bind(slot_i64)
                .bind(fee_event.protocol_name.clone())
                .bind(fee_event.program_id.clone())
                .bind(fee_event.event_kind.clone())
                .bind(fee_event.pool_account.clone())
                .bind(fee_event.actor_wallet.clone())
                .bind(fee_event.fee_token_mint.clone())
                .bind(fee_event.fee_amount_raw.clone())
                .bind(fee_event.payload_json.clone())
                .bind(fee_event.executed_at.to_rfc3339())
                .bind(fee_event.created_at.to_rfc3339())
                .execute(pool)
                .await;
                if let Err(error) = insert_result {
                    panic!("legacy parent insert must succeed: {}", error);
                }
                let id_result = sqlx::query_scalar::<sqlx::Sqlite, i64>(
                    r#"
SELECT id
FROM k_sol_fee_events
WHERE signature = ?
ORDER BY id DESC
LIMIT 1
                    "#,
                )
                .bind(fee_event.signature.clone())
                .fetch_one(pool)
                .await;
                match id_result {
                    Ok(id) => return id,
                    Err(error) => panic!("legacy parent id fetch must succeed: {}", error),
                }
            },
        }
    }

    #[tokio::test]
    async fn fee_events_upsert_with_amount_legs_normalizes_parent_id() {
        let database = make_database().await;
        let fee_event = make_fee_event(7101, 8101, "signature-helper", None, None);
        let legs = vec![
            make_leg(0, 0, crate::WSOL_MINT_ID, "100"),
            make_leg(0, 1, "TokenMint555", "200"),
        ];
        let upsert_result =
            crate::query_fee_events_upsert_with_amount_legs(&database, &fee_event, legs.as_slice())
                .await;
        let fee_event_id = match upsert_result {
            Ok(fee_event_id) => fee_event_id,
            Err(error) => panic!("upsert with legs must succeed: {}", error),
        };
        let list_result =
            crate::query_fee_event_amounts_list_by_fee_event_id(&database, fee_event_id).await;
        let listed = match list_result {
            Ok(listed) => listed,
            Err(error) => panic!("list must succeed: {}", error),
        };
        assert_eq!(listed.len(), 2);
        assert_eq!(listed[0].fee_event_id, fee_event_id);
        assert_eq!(listed[0].transaction_id, fee_event.transaction_id);
        assert_eq!(listed[0].decoded_event_id, fee_event.decoded_event_id);
        assert_eq!(listed[1].fee_event_id, fee_event_id);
        assert_eq!(listed[1].transaction_id, fee_event.transaction_id);
        assert_eq!(listed[1].decoded_event_id, fee_event.decoded_event_id);
    }

    #[tokio::test]
    async fn fee_events_upsert_automatically_creates_scalar_amount_leg() {
        let database = make_database().await;
        let fee_event = make_fee_event(
            7151,
            8151,
            "signature-auto-leg",
            Some(crate::WSOL_MINT_ID),
            Some("777"),
        );
        let upsert_result = crate::query_fee_events_upsert(&database, &fee_event).await;
        let fee_event_id = match upsert_result {
            Ok(fee_event_id) => fee_event_id,
            Err(error) => panic!("fee parent upsert must succeed: {}", error),
        };
        let list_result =
            crate::query_fee_event_amounts_list_by_fee_event_id(&database, fee_event_id).await;
        let listed = match list_result {
            Ok(listed) => listed,
            Err(error) => panic!("list must succeed: {}", error),
        };
        assert_eq!(listed.len(), 1);
        assert_eq!(listed[0].fee_event_id, fee_event_id);
        assert_eq!(listed[0].transaction_id, fee_event.transaction_id);
        assert_eq!(listed[0].decoded_event_id, fee_event.decoded_event_id);
        assert_eq!(listed[0].token_mint, crate::WSOL_MINT_ID);
        assert_eq!(listed[0].amount_raw, "777");
        assert_eq!(listed[0].amount_source, "parent_fee_event_amount");
    }

    #[tokio::test]
    async fn fee_event_amounts_backfill_single_leg_from_existing_parent() {
        let database = make_database().await;
        let fee_event = make_fee_event(
            7201,
            8201,
            "signature-backfill",
            Some(crate::WSOL_MINT_ID),
            Some("999"),
        );
        let fee_event_id = insert_legacy_fee_event_parent(&database, &fee_event).await;
        let backfill_result =
            crate::query_fee_event_amounts_backfill_single_leg_from_fee_events(&database).await;
        let backfilled = match backfill_result {
            Ok(backfilled) => backfilled,
            Err(error) => panic!("backfill must succeed: {}", error),
        };
        assert_eq!(backfilled, 1);
        let list_result =
            crate::query_fee_event_amounts_list_by_fee_event_id(&database, fee_event_id).await;
        let listed = match list_result {
            Ok(listed) => listed,
            Err(error) => panic!("list must succeed: {}", error),
        };
        assert_eq!(listed.len(), 1);
        assert_eq!(listed[0].token_mint, crate::WSOL_MINT_ID);
        assert_eq!(listed[0].amount_raw, "999");
        assert_eq!(listed[0].amount_source, "parent_fee_event_backfill");

        let second_backfill_result =
            crate::query_fee_event_amounts_backfill_single_leg_from_fee_events(&database).await;
        let second_backfilled = match second_backfill_result {
            Ok(second_backfilled) => second_backfilled,
            Err(error) => panic!("second backfill must succeed: {}", error),
        };
        assert_eq!(second_backfilled, 0);
    }

    #[tokio::test]
    async fn fee_event_amounts_delete_by_parent_removes_all_legs() {
        let database = make_database().await;
        let fee_event_id = 503;
        let legs = vec![
            make_leg(fee_event_id, 0, crate::WSOL_MINT_ID, "100"),
            make_leg(fee_event_id, 1, "TokenMint444", "200"),
        ];
        let replace_result =
            crate::query_fee_event_amounts_replace_for_fee_event(&database, fee_event_id, &legs)
                .await;
        if let Err(error) = replace_result {
            panic!("replace must succeed: {}", error);
        }

        let delete_result =
            crate::query_fee_event_amounts_delete_by_fee_event_id(&database, fee_event_id).await;
        let deleted = match delete_result {
            Ok(deleted) => deleted,
            Err(error) => panic!("delete must succeed: {}", error),
        };
        assert_eq!(deleted, 2);

        let list_result =
            crate::query_fee_event_amounts_list_by_fee_event_id(&database, fee_event_id).await;
        let listed = match list_result {
            Ok(listed) => listed,
            Err(error) => panic!("list must succeed: {}", error),
        };
        assert!(listed.is_empty());
    }
}
