// file: kb_lib/src/detect/service.rs

//! Detection persistence service.

/// Persistence façade between technical detection and database storage.
#[derive(Debug, Clone)]
pub struct DetectionPersistenceService {
    /// Shared database handle.
    database: std::sync::Arc<crate::Database>,
}

impl DetectionPersistenceService {
    /// Creates a new detection persistence service.
    pub fn new(database: std::sync::Arc<crate::Database>) -> Self {
        return Self { database };
    }
    /// Returns the shared database handle.
    pub fn database(&self) -> &std::sync::Arc<crate::Database> {
        return &self.database;
    }

    /// Persists one on-chain observation.
    pub async fn record_observation(
        &self,
        input: &crate::DetectionObservationInput,
    ) -> Result<i64, crate::Error> {
        let dto = crate::OnchainObservationDto::new(
            input.observation_kind.clone(),
            input.source_kind,
            input.endpoint_name.clone(),
            input.object_key.clone(),
            input.slot,
            input.payload.clone(),
        );
        return crate::query_onchain_observations_insert(self.database.as_ref(), &dto).await;
    }

    /// Persists one analysis signal.
    pub async fn record_signal(
        &self,
        input: &crate::DetectionSignalInput,
    ) -> Result<i64, crate::Error> {
        let dto = crate::AnalysisSignalDto::new(
            input.signal_kind.clone(),
            input.severity,
            input.object_key.clone(),
            input.related_observation_id,
            input.score,
            input.payload.clone(),
        );
        return crate::query_analysis_signals_insert(self.database.as_ref(), &dto).await;
    }

    /// Registers one token candidate from a technical source.
    ///
    /// This method:
    /// - upserts the normalized token entry,
    /// - stores the underlying technical observation,
    /// - stores the derived signal linked to that observation.
    pub async fn register_token_candidate(
        &self,
        input: &crate::DetectionTokenCandidateInput,
    ) -> Result<crate::DetectionTokenCandidateResult, crate::Error> {
        let token_dto = crate::TokenDto::new(
            input.mint.clone(),
            input.symbol.clone(),
            input.name.clone(),
            input.decimals,
            input.token_program.clone(),
            input.is_quote_token,
        );
        let token_id_result = crate::query_tokens_upsert(self.database.as_ref(), &token_dto).await;
        let token_id = match token_id_result {
            Ok(token_id) => token_id,
            Err(error) => return Err(error),
        };
        let observation_input = crate::DetectionObservationInput::new(
            input.observation_kind.clone(),
            input.source_kind,
            input.endpoint_name.clone(),
            input.mint.clone(),
            input.slot,
            input.observation_payload.clone(),
        );
        let observation_id_result = self.record_observation(&observation_input).await;
        let observation_id = match observation_id_result {
            Ok(observation_id) => observation_id,
            Err(error) => return Err(error),
        };
        let signal_payload = match &input.signal_payload {
            Some(signal_payload) => signal_payload.clone(),
            None => input.observation_payload.clone(),
        };
        let signal_input = crate::DetectionSignalInput::new(
            input.signal_kind.clone(),
            input.signal_severity,
            input.mint.clone(),
            Some(observation_id),
            input.signal_score,
            signal_payload,
        );
        let signal_id_result = self.record_signal(&signal_input).await;
        let signal_id = match signal_id_result {
            Ok(signal_id) => signal_id,
            Err(error) => return Err(error),
        };
        return Ok(crate::DetectionTokenCandidateResult { token_id, observation_id, signal_id });
    }

    /// Registers one pool candidate from a technical source.
    ///
    /// This method:
    /// - resolves one known DEX from the supplied program id,
    /// - upserts one normalized pool entry,
    /// - upserts one pool listing row,
    /// - stores the technical observation and derived signal.
    pub async fn register_pool_candidate(
        &self,
        input: &crate::DetectionPoolCandidateInput,
    ) -> Result<crate::DetectionPoolCandidateResult, crate::Error> {
        let dexes_result = crate::query_dexs_list(self.database.as_ref()).await;
        let dexes = match dexes_result {
            Ok(dexes) => dexes,
            Err(error) => return Err(error),
        };
        let mut matched_dex_option: std::option::Option<crate::DexDto> = None;
        for dex in dexes {
            let mut matched = false;
            if let Some(program_id) = &dex.program_id {
                if program_id == &input.dex_program_id {
                    matched = true;
                }
            }
            if !matched {
                if let Some(router_program_id) = &dex.router_program_id {
                    if router_program_id == &input.dex_program_id {
                        matched = true;
                    }
                }
            }
            if matched {
                matched_dex_option = Some(dex);
                break;
            }
        }
        let matched_dex = match matched_dex_option {
            Some(matched_dex) => matched_dex,
            None => {
                return Err(crate::Error::Db(format!(
                    "cannot register pool candidate: no known dex matches program id '{}'",
                    input.dex_program_id
                )));
            },
        };
        let dex_id = match matched_dex.id {
            Some(dex_id) => dex_id,
            None => {
                return Err(crate::Error::Db(
                    "cannot register pool candidate: matched dex has no id".to_string(),
                ));
            },
        };
        let pool_dto = crate::PoolDto::new(
            dex_id,
            input.pool_address.clone(),
            crate::PoolKind::Unknown,
            crate::PoolStatus::Pending,
        );
        let pool_id_result = crate::query_pools_upsert(self.database.as_ref(), &pool_dto).await;
        let pool_id = match pool_id_result {
            Ok(pool_id) => pool_id,
            Err(error) => return Err(error),
        };
        let listing_dto = crate::PoolListingDto::new(
            dex_id,
            pool_id,
            None,
            input.source_kind,
            input.endpoint_name.clone(),
            None,
            None,
            None,
        );
        let pool_listing_id_result =
            crate::query_pool_listings_upsert(self.database.as_ref(), &listing_dto).await;
        let pool_listing_id = match pool_listing_id_result {
            Ok(pool_listing_id) => pool_listing_id,
            Err(error) => return Err(error),
        };
        let observation_input = crate::DetectionObservationInput::new(
            input.observation_kind.clone(),
            input.source_kind,
            input.endpoint_name.clone(),
            input.pool_address.clone(),
            input.slot,
            input.observation_payload.clone(),
        );
        let observation_id_result = self.record_observation(&observation_input).await;
        let observation_id = match observation_id_result {
            Ok(observation_id) => observation_id,
            Err(error) => return Err(error),
        };
        let signal_payload = match &input.signal_payload {
            Some(signal_payload) => signal_payload.clone(),
            None => input.observation_payload.clone(),
        };
        let signal_input = crate::DetectionSignalInput::new(
            input.signal_kind.clone(),
            input.signal_severity,
            input.pool_address.clone(),
            Some(observation_id),
            input.signal_score,
            signal_payload,
        );
        let signal_id_result = self.record_signal(&signal_input).await;
        let signal_id = match signal_id_result {
            Ok(signal_id) => signal_id,
            Err(error) => return Err(error),
        };
        return Ok(crate::DetectionPoolCandidateResult {
            dex_id,
            pool_id,
            pool_listing_id,
            observation_id,
            signal_id,
        });
    }
}

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

    #[tokio::test]
    async fn record_observation_and_signal_work() {
        let database = create_database().await;
        let service = crate::DetectionPersistenceService::new(std::sync::Arc::new(database));
        let observation_id = service
            .record_observation(&crate::DetectionObservationInput::new(
                "rpc.program_notification".to_string(),
                crate::ObservationSourceKind::WsRpc,
                Some("mainnet_public_ws_slots".to_string()),
                crate::WSOL_MINT_ID.to_string(),
                Some(123456),
                serde_json::json!({
                    "mint": crate::WSOL_MINT_ID
                }),
            ))
            .await
            .expect("record observation must succeed");
        assert!(observation_id > 0);
        let signal_id = service
            .record_signal(&crate::DetectionSignalInput::new(
                "rpc.token_candidate".to_string(),
                crate::AnalysisSignalSeverity::Low,
                crate::WSOL_MINT_ID.to_string(),
                Some(observation_id),
                Some(0.25),
                serde_json::json!({
                    "reason": "test"
                }),
            ))
            .await
            .expect("record signal must succeed");
        assert!(signal_id > 0);
        let observations =
            crate::query_onchain_observations_list_recent(service.database().as_ref(), 10)
                .await
                .expect("list observations must succeed");
        let signals = crate::query_analysis_signals_list(service.database().as_ref(), 10)
            .await
            .expect("list signals must succeed");
        assert_eq!(observations.len(), 1);
        assert_eq!(signals.len(), 1);
        assert_eq!(observations[0].object_key, crate::WSOL_MINT_ID);
        assert_eq!(signals[0].object_key, crate::WSOL_MINT_ID);
    }

    #[tokio::test]
    async fn register_token_candidate_persists_token_observation_and_signal() {
        let database = create_database().await;
        let service = crate::DetectionPersistenceService::new(std::sync::Arc::new(database));
        let result = service
            .register_token_candidate(&crate::DetectionTokenCandidateInput::new(
                "Mint111111111111111111111111111111111111111".to_string(),
                Some("TEST".to_string()),
                Some("Test Token".to_string()),
                Some(6),
                crate::SPL_TOKEN_PROGRAM_ID.to_string(),
                false,
                crate::ObservationSourceKind::WsRpc,
                Some("helius_primary_ws_programs".to_string()),
                Some(777777),
                "rpc.token_candidate".to_string(),
                serde_json::json!({
                    "mint": "Mint111111111111111111111111111111111111111",
                    "source": "ws"
                }),
                "signal.token_candidate".to_string(),
                crate::AnalysisSignalSeverity::Medium,
                Some(0.70),
                None,
            ))
            .await
            .expect("register token candidate must succeed");
        assert!(result.token_id > 0);
        assert!(result.observation_id > 0);
        assert!(result.signal_id > 0);
        let token = crate::query_tokens_get_by_mint(
            service.database().as_ref(),
            "Mint111111111111111111111111111111111111111",
        )
        .await
        .expect("get token must succeed");
        assert!(token.is_some());
        assert_eq!(token.expect("token must exist").symbol.as_deref(), Some("TEST"));
        let observations =
            crate::query_onchain_observations_list_recent(service.database().as_ref(), 10)
                .await
                .expect("list observations must succeed");
        let signals = crate::query_analysis_signals_list(service.database().as_ref(), 10)
            .await
            .expect("list signals must succeed");
        assert_eq!(observations.len(), 1);
        assert_eq!(signals.len(), 1);
        assert_eq!(observations[0].object_key, "Mint111111111111111111111111111111111111111");
        assert_eq!(signals[0].object_key, "Mint111111111111111111111111111111111111111");
        assert_eq!(signals[0].related_observation_id, Some(result.observation_id));
    }

    #[tokio::test]
    async fn register_pool_candidate_persists_pool_listing_observation_and_signal() {
        let database = create_database().await;
        let service = crate::DetectionPersistenceService::new(std::sync::Arc::new(database));
        let dex_id = crate::query_dexs_upsert(
            service.database().as_ref(),
            &crate::DexDto::new(
                "raydium_amm_v4".to_string(),
                "Raydium AMM v4".to_string(),
                Some("DexProgram111111111111111111111111111111111".to_string()),
                None,
                true,
            ),
        )
        .await
        .expect("upsert dex must succeed");
        assert!(dex_id > 0);
        let result = service
            .register_pool_candidate(&crate::DetectionPoolCandidateInput::new(
                "Pool111111111111111111111111111111111111111".to_string(),
                "DexProgram111111111111111111111111111111111".to_string(),
                crate::ObservationSourceKind::WsRpc,
                Some("helius_primary_ws_programs".to_string()),
                Some(999999),
                "ws.program_notification".to_string(),
                serde_json::json!({
                    "pool": "Pool111111111111111111111111111111111111111"
                }),
                "signal.pool_candidate.raydium".to_string(),
                crate::AnalysisSignalSeverity::Medium,
                None,
                None,
            ))
            .await
            .expect("register pool candidate must succeed");
        assert!(result.dex_id > 0);
        assert!(result.pool_id > 0);
        assert!(result.pool_listing_id > 0);
        assert!(result.observation_id > 0);
        assert!(result.signal_id > 0);
        let pool = crate::query_pools_get_by_address(
            service.database().as_ref(),
            "Pool111111111111111111111111111111111111111",
        )
        .await
        .expect("get pool must succeed");
        assert!(pool.is_some());
        let pool = pool.expect("pool must exist");
        assert_eq!(pool.status, crate::PoolStatus::Pending);
        let listing =
            crate::query_pool_listings_get_by_pool_id(service.database().as_ref(), result.pool_id)
                .await
                .expect("get listing must succeed");
        assert!(listing.is_some());
    }
}
