use std::collections::HashMap;

use anyhow::Context;
use komodo_client::{
  api::read::{
    ExportAllResourcesToToml, ExportAllResourcesToTomlResponse,
    ExportResourcesToToml, ExportResourcesToTomlResponse,
    GetUserGroup, ListUserTargetPermissions,
  },
  entities::{
    alerter::Alerter,
    build::Build,
    builder::Builder,
    deployment::Deployment,
    permission::{PermissionLevel, UserTarget},
    procedure::Procedure,
    repo::Repo,
    resource::ResourceQuery,
    server::Server,
    server_template::ServerTemplate,
    stack::Stack,
    sync::ResourceSync,
    toml::{PermissionToml, ResourcesToml, UserGroupToml},
    user::User,
    ResourceTarget,
  },
};
use mungos::find::find_collect;
use resolver_api::Resolve;

use crate::{
  helpers::query::{get_id_to_tags, get_user_user_group_ids},
  resource,
  state::{db_client, State},
  sync::{
    toml::{convert_resource, ToToml, TOML_PRETTY_OPTIONS},
    AllResourcesById,
  },
};

impl Resolve<ExportAllResourcesToToml, User> for State {
  async fn resolve(
    &self,
    ExportAllResourcesToToml { tags }: ExportAllResourcesToToml,
    user: User,
  ) -> anyhow::Result<ExportAllResourcesToTomlResponse> {
    let mut targets = Vec::<ResourceTarget>::new();

    targets.extend(
      resource::list_for_user::<Alerter>(
        ResourceQuery::builder().tags(tags.clone()).build(),
        &user,
      )
      .await?
      .into_iter()
      .map(|resource| ResourceTarget::Alerter(resource.id)),
    );
    targets.extend(
      resource::list_for_user::<Builder>(
        ResourceQuery::builder().tags(tags.clone()).build(),
        &user,
      )
      .await?
      .into_iter()
      .map(|resource| ResourceTarget::Builder(resource.id)),
    );
    targets.extend(
      resource::list_for_user::<Server>(
        ResourceQuery::builder().tags(tags.clone()).build(),
        &user,
      )
      .await?
      .into_iter()
      .map(|resource| ResourceTarget::Server(resource.id)),
    );
    targets.extend(
      resource::list_for_user::<Deployment>(
        ResourceQuery::builder().tags(tags.clone()).build(),
        &user,
      )
      .await?
      .into_iter()
      .map(|resource| ResourceTarget::Deployment(resource.id)),
    );
    targets.extend(
      resource::list_for_user::<Stack>(
        ResourceQuery::builder().tags(tags.clone()).build(),
        &user,
      )
      .await?
      .into_iter()
      .map(|resource| ResourceTarget::Stack(resource.id)),
    );
    targets.extend(
      resource::list_for_user::<Build>(
        ResourceQuery::builder().tags(tags.clone()).build(),
        &user,
      )
      .await?
      .into_iter()
      .map(|resource| ResourceTarget::Build(resource.id)),
    );
    targets.extend(
      resource::list_for_user::<Repo>(
        ResourceQuery::builder().tags(tags.clone()).build(),
        &user,
      )
      .await?
      .into_iter()
      .map(|resource| ResourceTarget::Repo(resource.id)),
    );
    targets.extend(
      resource::list_for_user::<Procedure>(
        ResourceQuery::builder().tags(tags.clone()).build(),
        &user,
      )
      .await?
      .into_iter()
      .map(|resource| ResourceTarget::Procedure(resource.id)),
    );
    targets.extend(
      resource::list_for_user::<ServerTemplate>(
        ResourceQuery::builder().tags(tags.clone()).build(),
        &user,
      )
      .await?
      .into_iter()
      .map(|resource| ResourceTarget::ServerTemplate(resource.id)),
    );
    targets.extend(
      resource::list_full_for_user::<ResourceSync>(
        ResourceQuery::builder().tags(tags.clone()).build(),
        &user,
      )
      .await?
      .into_iter()
      // These will already be filtered by [ExportResourcesToToml]
      .map(|resource| ResourceTarget::ResourceSync(resource.id)),
    );

    let user_groups = if user.admin && tags.is_empty() {
      find_collect(&db_client().user_groups, None, None)
        .await
        .context("failed to query db for user groups")?
        .into_iter()
        .map(|user_group| user_group.id)
        .collect()
    } else {
      get_user_user_group_ids(&user.id).await?
    };

    self
      .resolve(
        ExportResourcesToToml {
          targets,
          user_groups,
          include_variables: tags.is_empty(),
        },
        user,
      )
      .await
  }
}

impl Resolve<ExportResourcesToToml, User> for State {
  async fn resolve(
    &self,
    ExportResourcesToToml {
      targets,
      user_groups,
      include_variables,
    }: ExportResourcesToToml,
    user: User,
  ) -> anyhow::Result<ExportResourcesToTomlResponse> {
    let mut res = ResourcesToml::default();
    let all = AllResourcesById::load().await?;
    let id_to_tags = get_id_to_tags(None).await?;
    for target in targets {
      match target {
        ResourceTarget::Alerter(id) => {
          let alerter = resource::get_check_permissions::<Alerter>(
            &id,
            &user,
            PermissionLevel::Read,
          )
          .await?;
          res
            .alerters
            .push(convert_resource::<Alerter>(alerter, &id_to_tags))
        }
        ResourceTarget::ResourceSync(id) => {
          let sync = resource::get_check_permissions::<ResourceSync>(
            &id,
            &user,
            PermissionLevel::Read,
          )
          .await?;
          if sync.config.file_contents.is_empty()
            && (sync.config.files_on_host
              || !sync.config.repo.is_empty())
          {
            res.resource_syncs.push(convert_resource::<ResourceSync>(
              sync,
              &id_to_tags,
            ))
          }
        }
        ResourceTarget::ServerTemplate(id) => {
          let template = resource::get_check_permissions::<
            ServerTemplate,
          >(
            &id, &user, PermissionLevel::Read
          )
          .await?;
          res.server_templates.push(
            convert_resource::<ServerTemplate>(template, &id_to_tags),
          )
        }
        ResourceTarget::Server(id) => {
          let server = resource::get_check_permissions::<Server>(
            &id,
            &user,
            PermissionLevel::Read,
          )
          .await?;
          res
            .servers
            .push(convert_resource::<Server>(server, &id_to_tags))
        }
        ResourceTarget::Builder(id) => {
          let mut builder =
            resource::get_check_permissions::<Builder>(
              &id,
              &user,
              PermissionLevel::Read,
            )
            .await?;
          Builder::replace_ids(&mut builder, &all);
          res
            .builders
            .push(convert_resource::<Builder>(builder, &id_to_tags))
        }
        ResourceTarget::Build(id) => {
          let mut build = resource::get_check_permissions::<Build>(
            &id,
            &user,
            PermissionLevel::Read,
          )
          .await?;
          Build::replace_ids(&mut build, &all);
          res
            .builds
            .push(convert_resource::<Build>(build, &id_to_tags))
        }
        ResourceTarget::Deployment(id) => {
          let mut deployment = resource::get_check_permissions::<
            Deployment,
          >(
            &id, &user, PermissionLevel::Read
          )
          .await?;
          Deployment::replace_ids(&mut deployment, &all);
          res.deployments.push(convert_resource::<Deployment>(
            deployment,
            &id_to_tags,
          ))
        }
        ResourceTarget::Repo(id) => {
          let mut repo = resource::get_check_permissions::<Repo>(
            &id,
            &user,
            PermissionLevel::Read,
          )
          .await?;
          Repo::replace_ids(&mut repo, &all);
          res.repos.push(convert_resource::<Repo>(repo, &id_to_tags))
        }
        ResourceTarget::Stack(id) => {
          let mut stack = resource::get_check_permissions::<Stack>(
            &id,
            &user,
            PermissionLevel::Read,
          )
          .await?;
          Stack::replace_ids(&mut stack, &all);
          res
            .stacks
            .push(convert_resource::<Stack>(stack, &id_to_tags))
        }
        ResourceTarget::Procedure(id) => {
          let mut procedure = resource::get_check_permissions::<
            Procedure,
          >(
            &id, &user, PermissionLevel::Read
          )
          .await?;
          Procedure::replace_ids(&mut procedure, &all);
          res.procedures.push(convert_resource::<Procedure>(
            procedure,
            &id_to_tags,
          ));
        }
        ResourceTarget::System(_) => continue,
      };
    }

    add_user_groups(user_groups, &mut res, &all, &user)
      .await
      .context("failed to add user groups")?;

    if include_variables {
      res.variables =
        find_collect(&db_client().variables, None, None)
          .await
          .context("failed to get variables from db")?
          .into_iter()
          .map(|mut variable| {
            if !user.admin && variable.is_secret {
              variable.value = "#".repeat(variable.value.len())
            }
            variable
          })
          .collect();
    }

    let toml = serialize_resources_toml(res)
      .context("failed to serialize resources to toml")?;

    Ok(ExportResourcesToTomlResponse { toml })
  }
}

async fn add_user_groups(
  user_groups: Vec<String>,
  res: &mut ResourcesToml,
  all: &AllResourcesById,
  user: &User,
) -> anyhow::Result<()> {
  let db = db_client();

  let usernames = find_collect(&db.users, None, None)
    .await?
    .into_iter()
    .map(|user| (user.id, user.username))
    .collect::<HashMap<_, _>>();

  for user_group in user_groups {
    let ug = State
      .resolve(GetUserGroup { user_group }, user.clone())
      .await?;
    // this method is admin only, but we already know user can see user group if above does not return Err
    let permissions = State
      .resolve(
        ListUserTargetPermissions {
          user_target: UserTarget::UserGroup(ug.id),
        },
        User {
          admin: true,
          ..Default::default()
        },
      )
      .await?
      .into_iter()
      .map(|mut permission| {
        match &mut permission.resource_target {
          ResourceTarget::Build(id) => {
            *id = all
              .builds
              .get(id)
              .map(|r| r.name.clone())
              .unwrap_or_default()
          }
          ResourceTarget::Builder(id) => {
            *id = all
              .builders
              .get(id)
              .map(|r| r.name.clone())
              .unwrap_or_default()
          }
          ResourceTarget::Deployment(id) => {
            *id = all
              .deployments
              .get(id)
              .map(|r| r.name.clone())
              .unwrap_or_default()
          }
          ResourceTarget::Server(id) => {
            *id = all
              .servers
              .get(id)
              .map(|r| r.name.clone())
              .unwrap_or_default()
          }
          ResourceTarget::Repo(id) => {
            *id = all
              .repos
              .get(id)
              .map(|r| r.name.clone())
              .unwrap_or_default()
          }
          ResourceTarget::Alerter(id) => {
            *id = all
              .alerters
              .get(id)
              .map(|r| r.name.clone())
              .unwrap_or_default()
          }
          ResourceTarget::Procedure(id) => {
            *id = all
              .procedures
              .get(id)
              .map(|r| r.name.clone())
              .unwrap_or_default()
          }
          ResourceTarget::ServerTemplate(id) => {
            *id = all
              .templates
              .get(id)
              .map(|r| r.name.clone())
              .unwrap_or_default()
          }
          ResourceTarget::ResourceSync(id) => {
            *id = all
              .syncs
              .get(id)
              .map(|r| r.name.clone())
              .unwrap_or_default()
          }
          ResourceTarget::Stack(id) => {
            *id = all
              .stacks
              .get(id)
              .map(|r| r.name.clone())
              .unwrap_or_default()
          }
          ResourceTarget::System(_) => {}
        }
        PermissionToml {
          target: permission.resource_target,
          level: permission.level,
        }
      })
      .collect();
    res.user_groups.push(UserGroupToml {
      name: ug.name,
      users: ug
        .users
        .into_iter()
        .filter_map(|user_id| usernames.get(&user_id).cloned())
        .collect(),
      all: ug.all,
      permissions,
    });
  }
  Ok(())
}

fn serialize_resources_toml(
  resources: ResourcesToml,
) -> anyhow::Result<String> {
  let mut toml = String::new();

  for server in resources.servers {
    if !toml.is_empty() {
      toml.push_str("\n\n##\n\n");
    }
    toml.push_str("[[server]]\n");
    Server::push_to_toml_string(server, &mut toml)?;
  }

  for stack in resources.stacks {
    if !toml.is_empty() {
      toml.push_str("\n\n##\n\n");
    }
    toml.push_str("[[stack]]\n");
    Stack::push_to_toml_string(stack, &mut toml)?;
  }

  for deployment in resources.deployments {
    if !toml.is_empty() {
      toml.push_str("\n\n##\n\n");
    }
    toml.push_str("[[deployment]]\n");
    Deployment::push_to_toml_string(deployment, &mut toml)?;
  }

  for build in resources.builds {
    if !toml.is_empty() {
      toml.push_str("\n\n##\n\n");
    }
    toml.push_str("[[build]]\n");
    Build::push_to_toml_string(build, &mut toml)?;
  }

  for repo in resources.repos {
    if !toml.is_empty() {
      toml.push_str("\n\n##\n\n");
    }
    toml.push_str("[[repo]]\n");
    Repo::push_to_toml_string(repo, &mut toml)?;
  }

  for procedure in resources.procedures {
    if !toml.is_empty() {
      toml.push_str("\n\n##\n\n");
    }
    toml.push_str("[[procedure]]\n");
    Procedure::push_to_toml_string(procedure, &mut toml)?;
  }

  for alerter in resources.alerters {
    if !toml.is_empty() {
      toml.push_str("\n\n##\n\n");
    }
    toml.push_str("[[alerter]]\n");
    Alerter::push_to_toml_string(alerter, &mut toml)?;
  }

  for builder in resources.builders {
    if !toml.is_empty() {
      toml.push_str("\n\n##\n\n");
    }
    toml.push_str("[[builder]]\n");
    Builder::push_to_toml_string(builder, &mut toml)?;
  }

  for server_template in resources.server_templates {
    if !toml.is_empty() {
      toml.push_str("\n\n##\n\n");
    }
    toml.push_str("[[server_template]]\n");
    ServerTemplate::push_to_toml_string(server_template, &mut toml)?;
  }

  for resource_sync in resources.resource_syncs {
    if !toml.is_empty() {
      toml.push_str("\n\n##\n\n");
    }
    toml.push_str("[[resource_sync]]\n");
    ResourceSync::push_to_toml_string(resource_sync, &mut toml)?;
  }

  for variable in &resources.variables {
    if !toml.is_empty() {
      toml.push_str("\n\n##\n\n");
    }
    toml.push_str("[[variable]]\n");
    toml.push_str(
      &toml_pretty::to_string(variable, TOML_PRETTY_OPTIONS)
        .context("failed to serialize variables to toml")?,
    );
  }

  for user_group in &resources.user_groups {
    if !toml.is_empty() {
      toml.push_str("\n\n##\n\n");
    }
    toml.push_str("[[user_group]]\n");
    toml.push_str(
      &toml_pretty::to_string(user_group, TOML_PRETTY_OPTIONS)
        .context("failed to serialize user_groups to toml")?,
    );
  }

  Ok(toml)
}
