use std::collections::VecDeque;
use std::sync::{Arc, OnceLock};

use anyhow::Result;
use tokio::sync::watch;

use crate::rtr::payload::{Payload, Timing};
use crate::rtr::store::RtrStore;

use super::core::{AppliedUpdate, CacheAvailability, RtrCache, RtrCacheBuilder, SessionIds};
use super::model::{Delta, Snapshot};

const VERSION_COUNT: usize = 3;
static STORE_SYNC_WORKER: OnceLock<watch::Sender<Option<StoreSyncJob>>> = OnceLock::new();

#[derive(Clone)]
struct StoreSyncJob {
    store: RtrStore,
    update: AppliedUpdate,
}

impl RtrCache {
    pub fn init(
        self,
        store: &RtrStore,
        max_delta: u8,
        prune_delta_by_snapshot_size: bool,
        timing: Timing,
        file_loader: impl Fn() -> Result<Vec<Payload>>,
    ) -> Result<Self> {
        if let Some(cache) =
            try_restore_from_store(store, max_delta, prune_delta_by_snapshot_size, timing)?
        {
            tracing::info!(
                "RTR cache restored from store: availability={:?}, session_ids={:?}, serials={:?}",
                cache.availability(),
                cache.session_ids(),
                cache.serials()
            );
            return Ok(cache);
        }

        tracing::warn!("RTR cache store unavailable or invalid, fallback to file loader");

        let payloads = file_loader()?;
        let source_snapshot = Snapshot::from_payloads(payloads);
        let availability = if source_snapshot.is_empty() {
            CacheAvailability::NoDataAvailable
        } else {
            CacheAvailability::Ready
        };
        let session_ids = SessionIds::random_distinct();
        let serial = if source_snapshot.is_empty() { 0 } else { 1 };

        let snapshots = std::array::from_fn(|version| {
            project_snapshot_for_version(&source_snapshot, version as u8)
        });
        let serials = [serial; VERSION_COUNT];
        let deltas =
            std::array::from_fn(|_| VecDeque::<Arc<Delta>>::with_capacity(max_delta as usize));

        tokio::spawn({
            let store = store.clone();
            let snapshots_for_store = snapshots.clone();
            let session_ids_for_store = session_ids.as_array();
            async move {
                let deltas_none: [Option<&Delta>; 3] = [None, None, None];
                let windows_none: [Option<(u32, u32)>; 3] = [None, None, None];
                let clear = [true, true, true];
                if let Err(e) = store.save_cache_state_versioned(
                    availability,
                    &snapshots_for_store,
                    &session_ids_for_store,
                    &serials,
                    &deltas_none,
                    &windows_none,
                    &clear,
                ) {
                    tracing::error!("persist cache state failed: {:?}", e);
                }
            }
        });

        Ok(RtrCacheBuilder::new()
            .availability(availability)
            .session_ids(session_ids)
            .max_delta(max_delta)
            .prune_delta_by_snapshot_size(prune_delta_by_snapshot_size)
            .timing(timing)
            .serials(serials)
            .snapshots(snapshots)
            .deltas_by_version(deltas)
            .build())
    }

    pub fn update(&mut self, new_payloads: Vec<Payload>, store: &RtrStore) -> Result<()> {
        if let Some(update) = self.apply_update(new_payloads)? {
            spawn_store_sync(store, update);
        }
        Ok(())
    }

    pub fn update_with_snapshot(&mut self, snapshot: Snapshot, store: &RtrStore) -> Result<()> {
        if let Some(update) = self.apply_update_from_snapshot(snapshot)? {
            spawn_store_sync(store, update);
        }
        Ok(())
    }
}

fn try_restore_from_store(
    store: &RtrStore,
    max_delta: u8,
    prune_delta_by_snapshot_size: bool,
    timing: Timing,
) -> Result<Option<RtrCache>> {
    let availability = store.get_availability()?;

    let mut snapshots = std::array::from_fn(|_| Snapshot::empty());
    let mut session_ids = [0u16; VERSION_COUNT];
    let mut serials = [0u32; VERSION_COUNT];
    let mut deltas =
        std::array::from_fn(|_| VecDeque::<Arc<Delta>>::with_capacity(max_delta as usize));

    for version in 0u8..=2 {
        let idx = version as usize;
        let snapshot = store.get_snapshot_for_version(version)?;
        let session_id = store.get_session_id_for_version(version)?;
        let serial = store.get_serial_for_version(version)?;
        let (snapshot, session_id, serial) = match (snapshot, session_id, serial) {
            (Some(snapshot), Some(session_id), Some(serial)) => (snapshot, session_id, serial),
            _ => return Ok(None),
        };
        snapshots[idx] = snapshot;
        session_ids[idx] = session_id;
        serials[idx] = serial;

        if availability == Some(CacheAvailability::NoDataAvailable) {
            continue;
        }

        if let Some((min_serial, max_serial)) = store.get_delta_window_for_version(version)? {
            let mut loaded =
                store.load_delta_window_for_version(version, min_serial, max_serial)?;
            let max_keep = usize::from(max_delta.max(1));
            if loaded.len() > max_keep {
                let drop_count = loaded.len() - max_keep;
                let dropped_serials = loaded
                    .iter()
                    .take(drop_count)
                    .map(Delta::serial)
                    .collect::<Vec<_>>();
                loaded.drain(..drop_count);
                tracing::warn!(
                    "RTR cache restore truncated persisted deltas to max_delta: version={}, max_delta={}, dropped_count={}, dropped_serials={:?}",
                    version,
                    max_delta,
                    drop_count,
                    dropped_serials
                );
            }
            deltas[idx] = loaded.into_iter().map(Arc::new).collect();
        }
    }

    let availability = availability.unwrap_or(CacheAvailability::Ready);
    Ok(Some(
        RtrCacheBuilder::new()
            .availability(availability)
            .session_ids(SessionIds::from_array(session_ids))
            .max_delta(max_delta)
            .prune_delta_by_snapshot_size(prune_delta_by_snapshot_size)
            .timing(timing)
            .serials(serials)
            .snapshots(snapshots)
            .deltas_by_version(deltas)
            .build(),
    ))
}

fn spawn_store_sync(store: &RtrStore, update: AppliedUpdate) {
    let tx = STORE_SYNC_WORKER.get_or_init(|| {
        let (tx, mut rx) = watch::channel::<Option<StoreSyncJob>>(None);
        tokio::spawn(async move {
            while rx.changed().await.is_ok() {
                let Some(job) = rx.borrow().clone() else {
                    continue;
                };
                persist_update_job(job);
            }
        });
        tx
    });

    let _ = tx.send_replace(Some(StoreSyncJob {
        store: store.clone(),
        update,
    }));
}

fn persist_update_job(job: StoreSyncJob) {
    let delta_refs: [Option<&Delta>; 3] =
        std::array::from_fn(|idx| job.update.deltas[idx].as_deref());
    if let Err(e) = job.store.save_cache_state_versioned(
        job.update.availability,
        &job.update.snapshots,
        &job.update.session_ids,
        &job.update.serials,
        &delta_refs,
        &job.update.delta_windows,
        &job.update.clear_delta_windows,
    ) {
        tracing::error!("persist cache state failed: {:?}", e);
    }
}

fn project_snapshot_for_version(snapshot: &Snapshot, version: u8) -> Snapshot {
    snapshot.project_for_version(version)
}
