use std::{
  sync::{Arc, OnceLock},
  time::Duration,
};

use anyhow::anyhow;
use async_timing_util::wait_until_timelength;
use database::mungos::find::find_collect;
use futures_util::future::join_all;
use komodo_client::entities::{
  docker::node::NodeState,
  komodo_timestamp,
  swarm::{Swarm, SwarmState},
};
use mogh_cache::CloneCache;
use periphery_client::api::swarm::{
  PollSwarmStatus, PollSwarmStatusResponse,
};
use tokio::sync::Mutex;

use crate::{
  config::monitoring_interval,
  helpers::swarm::swarm_request_custom_timeout,
  monitor::{
    RefreshCacheResources,
    resources::{
      update_swarm_deployment_cache, update_swarm_stack_cache,
    },
  },
  state::{CachedSwarmStatus, db_client, swarm_status_cache},
};

const ADDITIONAL_MS: u128 = 1000;

pub fn spawn_swarm_monitoring_loop() {
  tokio::spawn(async move {
    refresh_all_swarm_cache().await;
    let interval = monitoring_interval();
    loop {
      wait_until_timelength(interval, ADDITIONAL_MS).await;
      refresh_all_swarm_cache().await;
    }
  });
}

async fn refresh_all_swarm_cache() {
  let swarms =
    match find_collect(&db_client().swarms, None, None).await {
      Ok(swarms) => swarms,
      Err(e) => {
        error!(
          "Failed to get swarm list (refresh swarm cache) | {e:#}"
        );
        return;
      }
    };
  let futures = swarms.into_iter().map(|swarm| async move {
    refresh_swarm_cache(&swarm, false).await;
  });
  join_all(futures).await;
  // tokio::join!(check_alerts(ts), record_swarm_stats(ts));
}

/// Makes sure cache for swarm doesn't update too frequently / simultaneously.
/// If forced, will still block against simultaneous update.
fn refresh_swarm_cache_controller()
-> &'static CloneCache<String, Arc<Mutex<i64>>> {
  static CACHE: OnceLock<CloneCache<String, Arc<Mutex<i64>>>> =
    OnceLock::new();
  CACHE.get_or_init(Default::default)
}

/// The background loop will call this with force: false,
/// which exits early if the lock is busy or it was completed too recently.
/// If force is true, it will wait on simultaneous calls, and will
/// ignore the restriction on being completed too recently.
pub async fn refresh_swarm_cache(swarm: &Swarm, force: bool) {
  // Concurrency controller to ensure it isn't done too often
  // when it happens in other contexts.
  let controller = refresh_swarm_cache_controller()
    .get_or_insert_default(&swarm.id)
    .await;
  let mut lock = match controller.try_lock() {
    Ok(lock) => lock,
    Err(_) if force => controller.lock().await,
    Err(_) => return,
  };

  let now = komodo_timestamp();

  // early return if called again sooner than 1s.
  if !force && *lock > now - 1_000 {
    return;
  }

  *lock = now;

  let resources = RefreshCacheResources::load_swarm(swarm).await;

  if swarm.config.server_ids.is_empty() {
    resources.insert_status_unknown().await;
    swarm_status_cache()
      .insert(
        swarm.id.clone(),
        CachedSwarmStatus {
          id: swarm.id.clone(),
          state: SwarmState::Unknown,
          inspect: None,
          lists: None,
          err: Some(
            anyhow!("No servers configured as manager nodes").into(),
          ),
        }
        .into(),
      )
      .await;
    return;
  }

  let PollSwarmStatusResponse { inspect, lists } =
    match swarm_request_custom_timeout(
      &swarm.config.server_ids,
      PollSwarmStatus {},
      Duration::from_secs(1),
    )
    .await
    {
      Ok(info) => info,
      Err(e) => {
        resources.insert_status_unknown().await;
        swarm_status_cache()
          .insert(
            swarm.id.clone(),
            CachedSwarmStatus {
              id: swarm.id.clone(),
              state: SwarmState::Unknown,
              inspect: None,
              lists: None,
              err: Some(e.into()),
            }
            .into(),
          )
          .await;
        return;
      }
    };

  let mut state = SwarmState::Healthy;

  for node in &lists.nodes {
    if !matches!(node.state, Some(NodeState::READY)) {
      state = SwarmState::Unhealthy;
    }
  }

  tokio::join!(
    update_swarm_stack_cache(
      resources.stacks,
      &lists.stacks,
      &lists.services,
    ),
    update_swarm_deployment_cache(
      resources.deployments,
      &lists.services,
    )
  );

  swarm_status_cache()
    .insert(
      swarm.id.clone(),
      CachedSwarmStatus {
        id: swarm.id.clone(),
        state,
        inspect,
        lists: Some(lists),
        err: None,
      }
      .into(),
    )
    .await;
}
