// file: kb_lib/src/tx_model.rs

//! Projection of resolved transactions into normalized internal DB tables.

/// Service projecting resolved transaction JSON into internal chain tables.
#[derive(Debug, Clone)]
pub struct TransactionModelService {
    database: std::sync::Arc<crate::Database>,
}

impl TransactionModelService {
    /// Creates one transaction model projection service.
    pub fn new(database: std::sync::Arc<crate::Database>) -> Self {
        return Self { database };
    }

    /// Projects one resolved transaction JSON into slots / transactions / instructions tables.
    pub async fn persist_resolved_transaction(
        &self,
        signature: &str,
        source_endpoint_name: std::option::Option<std::string::String>,
        resolved_transaction: &serde_json::Value,
    ) -> Result<i64, crate::Error> {
        let slot_i64 = resolved_transaction.get("slot").and_then(serde_json::Value::as_i64);
        let slot_u64 = match slot_i64 {
            Some(slot_i64) => {
                let convert_result = u64::try_from(slot_i64);
                match convert_result {
                    Ok(slot_u64) => Some(slot_u64),
                    Err(error) => {
                        return Err(crate::Error::Db(format!(
                            "cannot convert resolved transaction slot '{}' to u64: {}",
                            slot_i64, error
                        )));
                    },
                }
            },
            None => None,
        };
        let block_time_unix =
            resolved_transaction.get("blockTime").and_then(serde_json::Value::as_i64);
        if let Some(slot_value) = slot_u64 {
            let slot_dto = crate::ChainSlotDto::new(slot_value, None, block_time_unix);
            let upsert_result =
                crate::query_chain_slots_upsert(self.database.as_ref(), &slot_dto).await;
            if let Err(error) = upsert_result {
                return Err(error);
            }
        }
        let version_text = extract_version_text(resolved_transaction);
        let err_json = extract_meta_err_json(resolved_transaction);
        let meta_json = extract_meta_json(resolved_transaction);
        let transaction_json_result = serde_json::to_string(resolved_transaction);
        let transaction_json = match transaction_json_result {
            Ok(transaction_json) => transaction_json,
            Err(error) => {
                return Err(crate::Error::Json(format!(
                    "cannot serialize resolved transaction '{}': {}",
                    signature, error
                )));
            },
        };
        let transaction_dto = crate::ChainTransactionDto::new(
            signature.to_string(),
            slot_u64,
            block_time_unix,
            source_endpoint_name,
            version_text,
            err_json,
            meta_json,
            transaction_json,
        );
        let transaction_id_result =
            crate::query_chain_transactions_upsert(self.database.as_ref(), &transaction_dto).await;
        let transaction_id = match transaction_id_result {
            Ok(transaction_id) => transaction_id,
            Err(error) => return Err(error),
        };
        let delete_result = crate::query_chain_instructions_delete_by_transaction_id(
            self.database.as_ref(),
            transaction_id,
        )
        .await;
        if let Err(error) = delete_result {
            return Err(error);
        }
        let outer_instructions = extract_outer_instructions(resolved_transaction);
        let mut outer_instruction_ids = std::collections::BTreeMap::<u32, i64>::new();
        for (index, instruction) in outer_instructions.iter().enumerate() {
            let index_result = u32::try_from(index);
            let instruction_index = match index_result {
                Ok(instruction_index) => instruction_index,
                Err(error) => {
                    return Err(crate::Error::Db(format!(
                        "cannot convert outer instruction index '{}' to u32: {}",
                        index, error
                    )));
                },
            };
            let dto_result =
                build_instruction_dto(transaction_id, None, instruction_index, None, instruction);
            let dto = match dto_result {
                Ok(dto) => dto,
                Err(error) => return Err(error),
            };
            let instruction_id_result =
                crate::query_chain_instructions_insert(self.database.as_ref(), &dto).await;
            let instruction_id = match instruction_id_result {
                Ok(instruction_id) => instruction_id,
                Err(error) => return Err(error),
            };
            outer_instruction_ids.insert(instruction_index, instruction_id);
        }
        let inner_instruction_groups = extract_inner_instruction_groups(resolved_transaction);
        for group in &inner_instruction_groups {
            let outer_index = match group.index {
                Some(outer_index) => outer_index,
                None => continue,
            };
            let parent_instruction_id_option = outer_instruction_ids.get(&outer_index).cloned();
            let parent_instruction_id = match parent_instruction_id_option {
                Some(parent_instruction_id) => parent_instruction_id,
                None => continue,
            };
            for (inner_index, inner_instruction) in group.instructions.iter().enumerate() {
                let inner_index_result = u32::try_from(inner_index);
                let inner_instruction_index = match inner_index_result {
                    Ok(inner_instruction_index) => inner_instruction_index,
                    Err(error) => {
                        return Err(crate::Error::Db(format!(
                            "cannot convert inner instruction index '{}' to u32: {}",
                            inner_index, error
                        )));
                    },
                };
                let dto_result = build_instruction_dto(
                    transaction_id,
                    Some(parent_instruction_id),
                    outer_index,
                    Some(inner_instruction_index),
                    inner_instruction,
                );
                let dto = match dto_result {
                    Ok(dto) => dto,
                    Err(error) => return Err(error),
                };
                let insert_result =
                    crate::query_chain_instructions_insert(self.database.as_ref(), &dto).await;
                if let Err(error) = insert_result {
                    return Err(error);
                }
            }
        }
        return Ok(transaction_id);
    }
}

#[derive(Debug, Clone)]
struct InnerInstructionGroup {
    index: std::option::Option<u32>,
    instructions: std::vec::Vec<serde_json::Value>,
}

fn extract_version_text(
    resolved_transaction: &serde_json::Value,
) -> std::option::Option<std::string::String> {
    let version_value_option = resolved_transaction.get("version");
    let version_value = match version_value_option {
        Some(version_value) => version_value,
        None => return None,
    };
    if let Some(version_text) = version_value.as_str() {
        return Some(version_text.to_string());
    }
    if let Some(version_number) = version_value.as_i64() {
        return Some(version_number.to_string());
    }
    return None;
}

fn extract_meta_json(
    resolved_transaction: &serde_json::Value,
) -> std::option::Option<std::string::String> {
    let meta_option = resolved_transaction.get("meta");
    let meta = match meta_option {
        Some(meta) => meta,
        None => return None,
    };
    let serialize_result = serde_json::to_string(meta);
    match serialize_result {
        Ok(json_text) => return Some(json_text),
        Err(_) => return None,
    }
}

fn extract_meta_err_json(
    resolved_transaction: &serde_json::Value,
) -> std::option::Option<std::string::String> {
    let meta_option = resolved_transaction.get("meta");
    let meta = match meta_option {
        Some(meta) => meta,
        None => return None,
    };
    let err_option = meta.get("err");
    let err = match err_option {
        Some(err) => err,
        None => return None,
    };
    if err.is_null() {
        return None;
    }
    let serialize_result = serde_json::to_string(err);
    match serialize_result {
        Ok(json_text) => return Some(json_text),
        Err(_) => return None,
    }
}

fn extract_outer_instructions(
    resolved_transaction: &serde_json::Value,
) -> std::vec::Vec<serde_json::Value> {
    let mut instructions = std::vec::Vec::new();
    let transaction_option = resolved_transaction.get("transaction");
    let transaction = match transaction_option {
        Some(transaction) => transaction,
        None => return instructions,
    };
    let message_option = transaction.get("message");
    let message = match message_option {
        Some(message) => message,
        None => return instructions,
    };
    let instructions_option = message.get("instructions");
    let instructions_value = match instructions_option {
        Some(instructions_value) => instructions_value,
        None => return instructions,
    };
    let array_option = instructions_value.as_array();
    let array = match array_option {
        Some(array) => array,
        None => return instructions,
    };
    for instruction in array {
        instructions.push(instruction.clone());
    }
    return instructions;
}

fn extract_inner_instruction_groups(
    resolved_transaction: &serde_json::Value,
) -> std::vec::Vec<InnerInstructionGroup> {
    let mut groups = std::vec::Vec::new();

    let meta_option = resolved_transaction.get("meta");
    let meta = match meta_option {
        Some(meta) => meta,
        None => return groups,
    };
    let inner_option = meta.get("innerInstructions");
    let inner_value = match inner_option {
        Some(inner_value) => inner_value,
        None => return groups,
    };
    let array_option = inner_value.as_array();
    let array = match array_option {
        Some(array) => array,
        None => return groups,
    };
    for group in array {
        let index = match group.get("index").and_then(serde_json::Value::as_i64) {
            Some(index_i64) => {
                let convert_result = u32::try_from(index_i64);
                match convert_result {
                    Ok(index_u32) => Some(index_u32),
                    Err(_) => None,
                }
            },
            None => None,
        };
        let mut instructions = std::vec::Vec::new();
        let instructions_option = group.get("instructions");
        if let Some(instructions_value) = instructions_option {
            if let Some(instructions_array) = instructions_value.as_array() {
                for instruction in instructions_array {
                    instructions.push(instruction.clone());
                }
            }
        }
        groups.push(InnerInstructionGroup { index, instructions });
    }
    return groups;
}

fn build_instruction_dto(
    transaction_id: i64,
    parent_instruction_id: std::option::Option<i64>,
    instruction_index: u32,
    inner_instruction_index: std::option::Option<u32>,
    instruction: &serde_json::Value,
) -> Result<crate::ChainInstructionDto, crate::Error> {
    let program_id = instruction
        .get("programId")
        .and_then(serde_json::Value::as_str)
        .map(std::string::ToString::to_string);
    let program_name = instruction
        .get("program")
        .and_then(serde_json::Value::as_str)
        .map(std::string::ToString::to_string);
    let stack_height = match instruction.get("stackHeight").and_then(serde_json::Value::as_i64) {
        Some(stack_height_i64) => {
            let stack_height_result = u32::try_from(stack_height_i64);
            match stack_height_result {
                Ok(stack_height) => Some(stack_height),
                Err(error) => {
                    return Err(crate::Error::Db(format!(
                        "cannot convert instruction stack_height '{}' to u32: {}",
                        stack_height_i64, error
                    )));
                },
            }
        },
        None => None,
    };
    let accounts_json = if let Some(accounts) = instruction.get("accounts") {
        let serialize_result = serde_json::to_string(accounts);
        match serialize_result {
            Ok(text) => text,
            Err(error) => {
                return Err(crate::Error::Json(format!(
                    "cannot serialize instruction accounts json: {}",
                    error
                )));
            },
        }
    } else {
        "[]".to_string()
    };
    let data_json = match instruction.get("data") {
        Some(data_value) => {
            let serialize_result = serde_json::to_string(data_value);
            match serialize_result {
                Ok(text) => Some(text),
                Err(error) => {
                    return Err(crate::Error::Json(format!(
                        "cannot serialize instruction data json: {}",
                        error
                    )));
                },
            }
        },
        None => None,
    };
    let parsed_type = instruction
        .get("parsed")
        .and_then(|parsed| return parsed.get("type"))
        .and_then(serde_json::Value::as_str)
        .map(std::string::ToString::to_string);
    let parsed_json = match instruction.get("parsed") {
        Some(parsed_value) => {
            let serialize_result = serde_json::to_string(parsed_value);
            match serialize_result {
                Ok(text) => Some(text),
                Err(error) => {
                    return Err(crate::Error::Json(format!(
                        "cannot serialize instruction parsed json: {}",
                        error
                    )));
                },
            }
        },
        None => None,
    };
    return Ok(crate::ChainInstructionDto::new(
        transaction_id,
        parent_instruction_id,
        instruction_index,
        inner_instruction_index,
        program_id,
        program_name,
        stack_height,
        accounts_json,
        data_json,
        parsed_type,
        parsed_json,
    ));
}

#[cfg(test)]
mod tests {
    async fn make_database() -> std::sync::Arc<crate::Database> {
        let tempdir = tempfile::tempdir().expect("tempdir must succeed");
        let database_path = tempdir.path().join("tx_model.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 = crate::Database::connect_and_initialize(&config)
            .await
            .expect("database init must succeed");
        return std::sync::Arc::new(database);
    }

    #[tokio::test]
    async fn persist_resolved_transaction_projects_outer_instructions() {
        let database = make_database().await;
        let service = crate::TransactionModelService::new(database.clone());
        let resolved_transaction = serde_json::json!({
            "slot": 700001,
            "blockTime": 1777000001,
            "version": 0,
            "transaction": {
                "message": {
                    "instructions": [
                        {
                            "programId": crate::SPL_TOKEN_PROGRAM_ID,
                            "program": "spl-token",
                            "stackHeight": 1,
                            "accounts": ["A111", "B222"],
                            "data": "raw-ix-0",
                            "parsed": {
                                "type": "transfer",
                                "info": { "amount": "100" }
                            }
                        },
                        {
                            "programId": crate::SYSTEM_PROGRAM_ID,
                            "program": "system",
                            "stackHeight": 1,
                            "accounts": ["C333", "D444"],
                            "data": "raw-ix-1"
                        }
                    ]
                }
            },
            "meta": {
                "err": null,
                "fee": 5000
            }
        });
        let transaction_id = service
            .persist_resolved_transaction(
                "sig-model-outer-1",
                Some("helius_primary_http".to_string()),
                &resolved_transaction,
            )
            .await
            .expect("persist_resolved_transaction must succeed");
        assert!(transaction_id > 0);
        let slot = crate::query_chain_slots_get(database.as_ref(), 700001)
            .await
            .expect("chain slot fetch must succeed")
            .expect("chain slot must exist");
        assert_eq!(slot.slot, 700001);
        assert_eq!(slot.block_time_unix, Some(1777000001));
        let transaction = crate::query_chain_transactions_get_by_signature(
            database.as_ref(),
            "sig-model-outer-1",
        )
        .await
        .expect("chain transaction fetch must succeed")
        .expect("chain transaction must exist");
        assert_eq!(transaction.id, Some(transaction_id));
        assert_eq!(transaction.slot, Some(700001));
        assert_eq!(transaction.source_endpoint_name, Some("helius_primary_http".to_string()));
        assert_eq!(transaction.version_text, Some("0".to_string()));
        let instructions = crate::query_chain_instructions_list_by_transaction_id(
            database.as_ref(),
            transaction_id,
        )
        .await
        .expect("chain instructions list must succeed");
        assert_eq!(instructions.len(), 2);
        assert_eq!(instructions[0].instruction_index, 0);
        assert_eq!(instructions[0].inner_instruction_index, None);
        assert_eq!(instructions[0].program_name, Some("spl-token".to_string()));
        assert_eq!(instructions[0].parsed_type, Some("transfer".to_string()));
        assert_eq!(instructions[1].instruction_index, 1);
        assert_eq!(instructions[1].inner_instruction_index, None);
        assert_eq!(instructions[1].program_name, Some("system".to_string()));
        assert_eq!(instructions[1].parsed_type, None);
    }

    #[tokio::test]
    async fn persist_resolved_transaction_projects_inner_instructions() {
        let database = make_database().await;
        let service = crate::TransactionModelService::new(database.clone());
        let resolved_transaction = serde_json::json!({
            "slot": 700002,
            "blockTime": 1777000002,
            "version": 0,
            "transaction": {
                "message": {
                    "instructions": [
                        {
                            "programId": "OuterProgram1111111111111111111111111111111",
                            "program": "outer-program",
                            "stackHeight": 1,
                            "accounts": ["PARENT1"],
                            "data": "raw-parent",
                            "parsed": {
                                "type": "outerInstruction",
                                "info": { "k": "v" }
                            }
                        }
                    ]
                }
            },
            "meta": {
                "err": null,
                "innerInstructions": [
                    {
                        "index": 0,
                        "instructions": [
                            {
                                "programId": crate::SPL_TOKEN_PROGRAM_ID,
                                "program": "spl-token",
                                "stackHeight": 2,
                                "accounts": ["INNER1", "INNER2"],
                                "data": "raw-inner-0",
                                "parsed": {
                                    "type": "mintTo",
                                    "info": { "amount": "5" }
                                }
                            },
                            {
                                "programId": crate::SPL_TOKEN_PROGRAM_ID,
                                "program": "spl-token",
                                "stackHeight": 2,
                                "accounts": ["INNER3", "INNER4"],
                                "data": "raw-inner-1",
                                "parsed": {
                                    "type": "burn",
                                    "info": { "amount": "2" }
                                }
                            }
                        ]
                    }
                ]
            }
        });
        let transaction_id = service
            .persist_resolved_transaction(
                "sig-model-inner-1",
                Some("mainnet_public_http".to_string()),
                &resolved_transaction,
            )
            .await
            .expect("persist_resolved_transaction must succeed");
        let instructions = crate::query_chain_instructions_list_by_transaction_id(
            database.as_ref(),
            transaction_id,
        )
        .await
        .expect("chain instructions list must succeed");
        assert_eq!(instructions.len(), 3);
        let parent = &instructions[0];
        assert_eq!(parent.parent_instruction_id, None);
        assert_eq!(parent.instruction_index, 0);
        assert_eq!(parent.inner_instruction_index, None);
        let inner_a = &instructions[1];
        assert_eq!(inner_a.parent_instruction_id, parent.id);
        assert_eq!(inner_a.instruction_index, 0);
        assert_eq!(inner_a.inner_instruction_index, Some(0));
        assert_eq!(inner_a.parsed_type, Some("mintTo".to_string()));
        let inner_b = &instructions[2];
        assert_eq!(inner_b.parent_instruction_id, parent.id);
        assert_eq!(inner_b.instruction_index, 0);
        assert_eq!(inner_b.inner_instruction_index, Some(1));
        assert_eq!(inner_b.parsed_type, Some("burn".to_string()));
    }

    #[tokio::test]
    async fn persist_resolved_transaction_replaces_existing_projection() {
        let database = make_database().await;
        let service = crate::TransactionModelService::new(database.clone());
        let first_resolved_transaction = serde_json::json!({
            "slot": 700003,
            "blockTime": 1777000003,
            "version": 0,
            "transaction": {
                "message": {
                    "instructions": [
                        {
                            "programId": "FirstProgram1111111111111111111111111111111",
                            "program": "first-program",
                            "stackHeight": 1,
                            "accounts": ["A1"],
                            "data": "raw-a1"
                        },
                        {
                            "programId": "SecondProgram111111111111111111111111111111",
                            "program": "second-program",
                            "stackHeight": 1,
                            "accounts": ["A2"],
                            "data": "raw-a2"
                        }
                    ]
                }
            },
            "meta": {
                "err": null
            }
        });
        let transaction_id_first = service
            .persist_resolved_transaction(
                "sig-model-replace-1",
                Some("helius_primary_http".to_string()),
                &first_resolved_transaction,
            )
            .await
            .expect("first projection must succeed");
        let first_instructions = crate::query_chain_instructions_list_by_transaction_id(
            database.as_ref(),
            transaction_id_first,
        )
        .await
        .expect("first chain instructions list must succeed");
        assert_eq!(first_instructions.len(), 2);
        let second_resolved_transaction = serde_json::json!({
            "slot": 700003,
            "blockTime": 1777000004,
            "version": 0,
            "transaction": {
                "message": {
                    "instructions": [
                        {
                            "programId": "OnlyProgram11111111111111111111111111111111",
                            "program": "only-program",
                            "stackHeight": 1,
                            "accounts": ["B1"],
                            "data": "raw-b1",
                            "parsed": {
                                "type": "singleInstruction",
                                "info": { "flag": true }
                            }
                        }
                    ]
                }
            },
            "meta": {
                "err": null
            }
        });
        let transaction_id_second = service
            .persist_resolved_transaction(
                "sig-model-replace-1",
                Some("helius_primary_http".to_string()),
                &second_resolved_transaction,
            )
            .await
            .expect("second projection must succeed");
        assert_eq!(transaction_id_first, transaction_id_second);
        let second_instructions = crate::query_chain_instructions_list_by_transaction_id(
            database.as_ref(),
            transaction_id_second,
        )
        .await
        .expect("second chain instructions list must succeed");
        assert_eq!(second_instructions.len(), 1);
        assert_eq!(second_instructions[0].instruction_index, 0);
        assert_eq!(second_instructions[0].program_name, Some("only-program".to_string()));
        assert_eq!(second_instructions[0].parsed_type, Some("singleInstruction".to_string()));
        let transaction = crate::query_chain_transactions_get_by_signature(
            database.as_ref(),
            "sig-model-replace-1",
        )
        .await
        .expect("transaction fetch must succeed")
        .expect("transaction must exist");
        assert_eq!(transaction.block_time_unix, Some(1777000004));
    }
}
