use alloy::primitives::B256;
use tokio::sync::mpsc::{self};
use tracing::{error, info, info_span};

use crate::{
    clients::beacon::types::{BlockHeader, BlockId, BlockIdResolution},
    context::{CommonContext, Context},
    indexer::{
        tasks::{
            indexing::{IndexingTask, RunParams as IndexingTaskRunParams},
            sse_indexing::{RunParams as SSEIndexingTaskRunParams, SSEIndexingTask},
        },
        types::{
            ErrorResport, IndexingTaskJoinHandle, TaskErrorChannelReceiver, TaskErrorChannelSender,
        },
    },
    synchronizer::{CheckpointType, CommonSynchronizer, SynchronizerBuilder},
};

use self::error::IndexerError;

pub mod error;
pub mod tasks;
pub mod types;

pub struct Indexer {
    context: Box<dyn CommonContext>,
    disable_backfill: bool,

    error_report_tx: TaskErrorChannelSender,
    error_report_rx: TaskErrorChannelReceiver,
}

pub type IndexerResult<T> = Result<T, IndexerError>;

impl Indexer {
    pub fn new(context: Context, disable_backfill: bool) -> Self {
        let (error_report_tx, error_report_rx) = mpsc::channel::<ErrorResport>(32);

        Self {
            context: Box::new(context),
            disable_backfill,
            error_report_rx,
            error_report_tx,
        }
    }

    pub async fn index_from(&mut self, from_block_id: BlockId) -> IndexerResult<()> {
        let slot = from_block_id
            .resolve_to_slot(self.context.beacon_client())
            .await?;

        self.start_sse_listening_task(SSEIndexingTaskRunParams {
            last_synced_block: None,
            last_synced_slot: Some(slot),
        });

        Ok(())
    }

    pub async fn index_block_range(
        &mut self,
        from_block_id: BlockId,
        to_block_id: BlockId,
    ) -> IndexerResult<()> {
        let mut builder = SynchronizerBuilder::new();

        builder.with_checkpoint(None);

        let mut synchronizer = builder.build(self.context.clone());

        synchronizer.sync_blocks(from_block_id, to_block_id).await?;

        Ok(())
    }
    pub async fn index(&mut self) -> IndexerResult<()> {
        let sync_state = match self.context.blobscan_client().get_sync_state().await {
            Ok(state) => state,
            Err(error) => {
                error!(?error, "Failed to fetch blobscan's sync state");

                return Err(IndexerError::IndexerStateRetrievalError(error));
            }
        };
        let lowest_synced_slot = sync_state
            .as_ref()
            .and_then(|state| state.last_lower_synced_slot);
        let last_synced_block = sync_state.as_ref().and_then(|state| {
            match (
                state.last_upper_synced_block_root,
                state.last_upper_synced_block_slot,
            ) {
                (Some(root), Some(slot)) => Some(BlockHeader {
                    parent_root: B256::ZERO,
                    root,
                    slot,
                }),
                _ => None,
            }
        });
        let last_synced_slot = sync_state
            .as_ref()
            .and_then(|state| state.last_upper_synced_slot);

        info!(
            lowest_synced_slot = ?lowest_synced_slot,
            last_synced_block_slot = ?last_synced_block.as_ref().map(|block| block.slot),
            last_synced_block_root = ?last_synced_block.as_ref().map(|block| block.root),
            "Starting indexer…",
        );

        let dencun_fork_slot = self.context.network().dencun_fork_slot;
        let backfill_completed = lowest_synced_slot.is_some_and(|slot| slot <= dencun_fork_slot);

        if !self.disable_backfill && !backfill_completed {
            let task = IndexingTask::new(
                "backfill",
                self.context.clone(),
                Some(info_span!("backfill")),
            );

            let current_lowest_block_id = match lowest_synced_slot {
                Some(lowest_synced_slot) => lowest_synced_slot.saturating_sub(1).into(),
                None => match last_synced_slot {
                    Some(last_synced_slot) => last_synced_slot.saturating_sub(1).into(),
                    None => BlockId::Head,
                },
            };

            task.run(IndexingTaskRunParams {
                error_report_tx: self.error_report_tx.clone(),
                result_report_tx: None,
                from_block_id: current_lowest_block_id,
                to_block_id: dencun_fork_slot.into(),
                prev_block: None,
                checkpoint: Some(CheckpointType::Lower),
            });
        }

        self.start_sse_listening_task(SSEIndexingTaskRunParams {
            last_synced_block,
            last_synced_slot,
        });

        if let Some(error_report) = self.error_report_rx.recv().await {
            return Err(IndexerError::IndexingTaskError {
                task_name: error_report.task_name,
                error: error_report.error,
            });
        }

        Ok(())
    }

    fn start_sse_listening_task(&self, params: SSEIndexingTaskRunParams) -> IndexingTaskJoinHandle {
        let task = SSEIndexingTask::new(self.context.clone(), self.error_report_tx.clone());

        task.run(params)
    }
}
