use std::collections::HashMap;

use anyhow::{anyhow, Context};
use komodo_client::{
  api::read::{GetUpdate, ListUpdates, ListUpdatesResponse},
  entities::{
    alerter::Alerter,
    build::Build,
    builder::Builder,
    deployment::Deployment,
    permission::PermissionLevel,
    procedure::Procedure,
    repo::Repo,
    server::Server,
    server_template::ServerTemplate,
    stack::Stack,
    sync::ResourceSync,
    update::{Update, UpdateListItem},
    user::User,
    ResourceTarget,
  },
};
use mungos::{
  by_id::find_one_by_id,
  find::find_collect,
  mongodb::{bson::doc, options::FindOptions},
};
use resolver_api::Resolve;

use crate::{
  config::core_config,
  resource,
  state::{db_client, State},
};

const UPDATES_PER_PAGE: i64 = 100;

impl Resolve<ListUpdates, User> for State {
  async fn resolve(
    &self,
    ListUpdates { query, page }: ListUpdates,
    user: User,
  ) -> anyhow::Result<ListUpdatesResponse> {
    let query = if user.admin || core_config().transparent_mode {
      query
    } else {
      let server_query =
        resource::get_resource_ids_for_user::<Server>(&user)
          .await?
          .map(|ids| {
            doc! {
              "target.type": "Server", "target.id": { "$in": ids }
            }
          })
          .unwrap_or_else(|| doc! { "target.type": "Server" });

      let deployment_query =
        resource::get_resource_ids_for_user::<Deployment>(&user)
          .await?
          .map(|ids| {
            doc! {
              "target.type": "Deployment", "target.id": { "$in": ids }
            }
          })
          .unwrap_or_else(|| doc! { "target.type": "Deployment" });

      let stack_query =
        resource::get_resource_ids_for_user::<Stack>(&user)
          .await?
          .map(|ids| {
            doc! {
              "target.type": "Stack", "target.id": { "$in": ids }
            }
          })
          .unwrap_or_else(|| doc! { "target.type": "Stack" });

      let build_query =
        resource::get_resource_ids_for_user::<Build>(&user)
          .await?
          .map(|ids| {
            doc! {
              "target.type": "Build", "target.id": { "$in": ids }
            }
          })
          .unwrap_or_else(|| doc! { "target.type": "Build" });

      let repo_query =
        resource::get_resource_ids_for_user::<Repo>(&user)
          .await?
          .map(|ids| {
            doc! {
              "target.type": "Repo", "target.id": { "$in": ids }
            }
          })
          .unwrap_or_else(|| doc! { "target.type": "Repo" });

      let procedure_query =
        resource::get_resource_ids_for_user::<Procedure>(&user)
          .await?
          .map(|ids| {
            doc! {
              "target.type": "Procedure", "target.id": { "$in": ids }
            }
          })
          .unwrap_or_else(|| doc! { "target.type": "Procedure" });

      let builder_query =
        resource::get_resource_ids_for_user::<Builder>(&user)
          .await?
          .map(|ids| {
            doc! {
              "target.type": "Builder", "target.id": { "$in": ids }
            }
          })
          .unwrap_or_else(|| doc! { "target.type": "Builder" });

      let alerter_query =
        resource::get_resource_ids_for_user::<Alerter>(&user)
          .await?
          .map(|ids| {
            doc! {
              "target.type": "Alerter", "target.id": { "$in": ids }
            }
          })
          .unwrap_or_else(|| doc! { "target.type": "Alerter" });

      let server_template_query = resource::get_resource_ids_for_user::<ServerTemplate>(
        &user,
      )
      .await?
      .map(|ids| {
        doc! {
          "target.type": "ServerTemplate", "target.id": { "$in": ids }
        }
      })
      .unwrap_or_else(|| doc! { "target.type": "ServerTemplate" });

      let resource_sync_query = resource::get_resource_ids_for_user::<ResourceSync>(
        &user,
      )
      .await?
      .map(|ids| {
        doc! {
          "target.type": "ResourceSync", "target.id": { "$in": ids }
        }
      })
      .unwrap_or_else(|| doc! { "target.type": "ResourceSync" });

      let mut query = query.unwrap_or_default();
      query.extend(doc! {
        "$or": [
          server_query,
          deployment_query,
          stack_query,
          build_query,
          repo_query,
          procedure_query,
          alerter_query,
          builder_query,
          server_template_query,
          resource_sync_query,
        ]
      });
      query.into()
    };

    let usernames = find_collect(&db_client().users, None, None)
      .await
      .context("failed to pull users from db")?
      .into_iter()
      .map(|u| (u.id, u.username))
      .collect::<HashMap<_, _>>();

    let updates = find_collect(
      &db_client().updates,
      query,
      FindOptions::builder()
        .sort(doc! { "start_ts": -1 })
        .skip(page as u64 * UPDATES_PER_PAGE as u64)
        .limit(UPDATES_PER_PAGE)
        .build(),
    )
    .await
    .context("failed to pull updates from db")?
    .into_iter()
    .map(|u| {
      let username = if User::is_service_user(&u.operator) {
        u.operator.clone()
      } else {
        usernames
          .get(&u.operator)
          .cloned()
          .unwrap_or("unknown".to_string())
      };
      UpdateListItem {
        username,
        id: u.id,
        operation: u.operation,
        start_ts: u.start_ts,
        success: u.success,
        operator: u.operator,
        target: u.target,
        status: u.status,
        version: u.version,
        other_data: u.other_data,
      }
    })
    .collect::<Vec<_>>();

    let next_page = if updates.len() == UPDATES_PER_PAGE as usize {
      Some(page + 1)
    } else {
      None
    };

    Ok(ListUpdatesResponse { updates, next_page })
  }
}

impl Resolve<GetUpdate, User> for State {
  async fn resolve(
    &self,
    GetUpdate { id }: GetUpdate,
    user: User,
  ) -> anyhow::Result<Update> {
    let update = find_one_by_id(&db_client().updates, &id)
      .await
      .context("failed to query to db")?
      .context("no update exists with given id")?;
    if user.admin || core_config().transparent_mode {
      return Ok(update);
    }
    match &update.target {
      ResourceTarget::System(_) => {
        return Err(anyhow!(
          "user must be admin to view system updates"
        ))
      }
      ResourceTarget::Server(id) => {
        resource::get_check_permissions::<Server>(
          id,
          &user,
          PermissionLevel::Read,
        )
        .await?;
      }
      ResourceTarget::Deployment(id) => {
        resource::get_check_permissions::<Deployment>(
          id,
          &user,
          PermissionLevel::Read,
        )
        .await?;
      }
      ResourceTarget::Build(id) => {
        resource::get_check_permissions::<Build>(
          id,
          &user,
          PermissionLevel::Read,
        )
        .await?;
      }
      ResourceTarget::Repo(id) => {
        resource::get_check_permissions::<Repo>(
          id,
          &user,
          PermissionLevel::Read,
        )
        .await?;
      }
      ResourceTarget::Builder(id) => {
        resource::get_check_permissions::<Builder>(
          id,
          &user,
          PermissionLevel::Read,
        )
        .await?;
      }
      ResourceTarget::Alerter(id) => {
        resource::get_check_permissions::<Alerter>(
          id,
          &user,
          PermissionLevel::Read,
        )
        .await?;
      }
      ResourceTarget::Procedure(id) => {
        resource::get_check_permissions::<Procedure>(
          id,
          &user,
          PermissionLevel::Read,
        )
        .await?;
      }
      ResourceTarget::ServerTemplate(id) => {
        resource::get_check_permissions::<ServerTemplate>(
          id,
          &user,
          PermissionLevel::Read,
        )
        .await?;
      }
      ResourceTarget::ResourceSync(id) => {
        resource::get_check_permissions::<ResourceSync>(
          id,
          &user,
          PermissionLevel::Read,
        )
        .await?;
      }
      ResourceTarget::Stack(id) => {
        resource::get_check_permissions::<Stack>(
          id,
          &user,
          PermissionLevel::Read,
        )
        .await?;
      }
    }
    Ok(update)
  }
}
