import { CaminoApiError } from '../../types'
import { HTTP_STATUS } from 'camino-common/src/http'
import { User, UserNotNull, UtilisateurId } from 'camino-common/src/roles'
import { utilisateursFormatTable } from './format/utilisateurs'
import { tableConvert } from './_convert'
import { fileNameCreate } from '../../tools/file-name-create'
import { QGISTokenRest, qgisTokenValidator, utilisateursSearchParamsValidator, UtilisateursTable } from 'camino-common/src/utilisateur'
import { idGenerate } from '../../database/models/_format/id-create'
import { utilisateurUpdationValidate, UtilisateurUpdationValidateErrors } from '../../business/validations/utilisateur-updation-validate'
import { canDeleteUtilisateur } from 'camino-common/src/permissions/utilisateurs'
import { Pool } from 'pg'
import { isNotNullNorUndefined, isNullOrUndefined } from 'camino-common/src/typescript-tools'
import { config } from '../../config/index'
import { Effect, Match } from 'effect'
import { RestNewGetCall, RestNewPostCall } from '../../server/rest'
import {
  getKeycloakIdByUserId,
  GetKeycloakIdByUserIdErrors,
  GetUtilisateurByIdErrors,
  getUtilisateursFilteredAndSorted,
  GetUtilisateursFilteredAndSortedErrors,
  newGetUtilisateurById,
  softDeleteUtilisateur,
  updateUtilisateurRole,
} from '../../database/queries/utilisateurs.queries'
import { EffectDbQueryAndValidateErrors } from '../../pg-database'
import { callAndExit, shortCircuitError, zodParseEffectTyped } from '../../tools/fp-tools'
import { z } from 'zod'
import { getEntreprises } from './entreprises.queries'
import { fetch } from 'undici'
import { updateQgisToken } from './utilisateurs.queries'

type UpdateUtilisateurPermissionErrors = EffectDbQueryAndValidateErrors | GetUtilisateurByIdErrors | UtilisateurUpdationValidateErrors
export const updateUtilisateurPermission: RestNewPostCall<'/rest/utilisateurs/:id/permission'> = (rootPipe): Effect.Effect<{ id: UtilisateurId }, CaminoApiError<UpdateUtilisateurPermissionErrors>> =>
  rootPipe.pipe(
    Effect.bind('utilisateurOld', ({ params, pool, user }) => newGetUtilisateurById(pool, params.id, user)),
    Effect.tap(({ user, body, utilisateurOld }) => utilisateurUpdationValidate(user, body, utilisateurOld)),
    Effect.tap(({ body, pool }) => updateUtilisateurRole(pool, body)),
    Effect.map(({ params }) => ({ id: params.id })),
    Effect.mapError(caminoError =>
      Match.value(caminoError.message).pipe(
        Match.when("L'utilisateur est invalide", () => ({ ...caminoError, status: HTTP_STATUS.INTERNAL_SERVER_ERROR })),
        Match.whenOr(
          'droits insuffisants',
          "Impossible d'exécuter la requête dans la base de données",
          'Les données en base ne correspondent pas à ce qui est attendu',
          'utilisateur incorrect',
          "l'utilisateur n'existe pas",
          'droits insuffisants',
          'impossible de modifier son propre rôle',
          'droits insuffisants pour modifier les rôles',
          'droits insuffisants pour modifier les administrations',
          'droits insuffisants pour modifier les entreprises',
          () => ({
            ...caminoError,
            status: HTTP_STATUS.BAD_REQUEST,
          })
        ),
        Match.exhaustive
      )
    )
  )

export type KeycloakAccessTokenResponse = { access_token: string }

const getKeycloakApiToken = async (): Promise<string> => {
  const client_id = config().KEYCLOAK_API_CLIENT_ID
  const client_secret = config().KEYCLOAK_API_CLIENT_SECRET
  const url = config().KEYCLOAK_URL

  if (!client_id || !client_secret || !url) {
    throw new Error('variables KEYCLOAK_API_CLIENT_ID and KEYCLOAK_API_CLIENT_SECRET and KEYCLOAK_URL must be set')
  }

  const response = await fetch(`${url}/realms/Camino/protocol/openid-connect/token`, {
    method: 'POST',
    body: `grant_type=client_credentials&client_id=${client_id}&client_secret=${client_secret}`,
    headers: {
      Accept: 'application/json',
      'Content-Type': 'application/x-www-form-urlencoded',
    },
  })

  if (response.ok) {
    const responseBody = (await response.json()) as KeycloakAccessTokenResponse

    const token = responseBody.access_token
    if (isNullOrUndefined(token)) {
      throw new Error('token vide')
    }

    return token
  } else {
    console.error('error', await response.text())
    throw new Error('Pas de token')
  }
}

const droitsInsuffisantsPourSupprimerLUtilisateur = "Droits insuffisants pour supprimer l'utilisateur" as const
const erreurLorsDeLaSuppressionDeLUtilisateur = "Erreur lors de la suppression de l'utilisateur" as const

type DeleteUtilisateurErrors = GetKeycloakIdByUserIdErrors | typeof droitsInsuffisantsPourSupprimerLUtilisateur | typeof erreurLorsDeLaSuppressionDeLUtilisateur
export const deleteUtilisateur: RestNewGetCall<'/rest/utilisateurs/:id/delete'> = (rootPipe): Effect.Effect<{ id: UtilisateurId }, CaminoApiError<DeleteUtilisateurErrors>> =>
  rootPipe.pipe(
    Effect.let('utilisateurId', ({ params }) => params.id),
    Effect.filterOrFail(
      ({ user, utilisateurId }) => canDeleteUtilisateur(user, utilisateurId),
      () => ({ message: droitsInsuffisantsPourSupprimerLUtilisateur })
    ),
    Effect.bind('keycloakId', ({ pool, utilisateurId }) => getKeycloakIdByUserId(pool, utilisateurId)),
    Effect.tap(({ keycloakId }) =>
      Effect.tryPromise({
        try: async () => {
          const authorizationToken = await getKeycloakApiToken()

          const deleteFromKeycloak = await fetch(`${config().KEYCLOAK_URL}/admin/realms/Camino/users/${keycloakId}`, {
            method: 'DELETE',
            headers: {
              authorization: `Bearer ${authorizationToken}`,
            },
          })
          if (!deleteFromKeycloak.ok) {
            throw new Error(`une erreur est apparue durant la suppression de l'utilisateur sur keycloak`)
          }
        },
        catch: e => ({ message: erreurLorsDeLaSuppressionDeLUtilisateur, extra: e }),
      })
    ),
    Effect.tap(({ pool, utilisateurId }) => softDeleteUtilisateur(pool, utilisateurId)),

    Effect.tap(({ utilisateurId, redirect, user }) => {
      if (isNotNullNorUndefined(user) && user.id === utilisateurId) {
        const uiUrl = config().OAUTH_URL
        const oauthLogoutUrl = new URL(`${uiUrl}/oauth2/sign_out`)
        redirect(oauthLogoutUrl.href)
      }
    }),
    Effect.map(({ utilisateurId }) => ({ id: utilisateurId })),
    Effect.mapError(caminoError =>
      Match.value(caminoError.message).pipe(
        Match.when("Droits insuffisants pour supprimer l'utilisateur", () => ({ ...caminoError, status: HTTP_STATUS.FORBIDDEN })),
        Match.when("Erreur lors de la suppression de l'utilisateur", () => ({ ...caminoError, status: HTTP_STATUS.INTERNAL_SERVER_ERROR })),
        Match.whenOr("Impossible de trouver l'utilisateur", "Impossible d'exécuter la requête dans la base de données", 'Les données en base ne correspondent pas à ce qui est attendu', () => ({
          ...caminoError,
          status: HTTP_STATUS.BAD_REQUEST,
        })),
        Match.exhaustive
      )
    )
  )

export const moi: RestNewGetCall<'/moi'> = (rootPipe): Effect.Effect<User, CaminoApiError<GetUtilisateurByIdErrors>> => {
  return rootPipe.pipe(
    Effect.tap(({ cookie }) => cookie.clearConnectedCookie()),
    Effect.filterOrFail(
      (value): value is typeof value & { user: UserNotNull } => isNotNullNorUndefined(value.user),
      () => shortCircuitError('utilisateur non connecté')
    ),
    Effect.bind('myUser', ({ user, pool }) => newGetUtilisateurById(pool, user.id, user)),
    Effect.tap(({ cookie }) => cookie.addConnectedCookie()),
    Effect.map(({ myUser }) => myUser),
    Effect.catchTag('utilisateur non connecté', _myError => Effect.succeed(null)),
    Effect.mapError(caminoError =>
      Match.value(caminoError.message).pipe(
        Match.when('droits insuffisants', () => ({ ...caminoError, status: HTTP_STATUS.FORBIDDEN })),
        Match.when('Les données en base ne correspondent pas à ce qui est attendu', () => ({ ...caminoError, status: HTTP_STATUS.INTERNAL_SERVER_ERROR })),
        Match.whenOr("Impossible d'exécuter la requête dans la base de données", "L'utilisateur est invalide", () => ({ ...caminoError, status: HTTP_STATUS.BAD_REQUEST })),

        Match.exhaustive
      )
    )
  )
}

const impossibleDeGenererUnTokenQgis = 'impossible de générer un token Qgis' as const
export const generateQgisToken: RestNewPostCall<'/rest/utilisateur/generateQgisToken'> = (
  rootPipe
): Effect.Effect<QGISTokenRest, CaminoApiError<EffectDbQueryAndValidateErrors | typeof impossibleDeGenererUnTokenQgis>> =>
  rootPipe.pipe(
    Effect.bind('token', () => zodParseEffectTyped(qgisTokenValidator, idGenerate(), impossibleDeGenererUnTokenQgis)),
    Effect.tap(({ token, pool, user }) => updateQgisToken(pool, user, token)),
    Effect.map(({ token, user }) => ({ token, url: `https://${user.email.replace('@', '%40')}:${token}@${config().API_HOST}/titres_qgis?` })),
    Effect.mapError(caminoError =>
      Match.value(caminoError.message).pipe(
        Match.whenOr("Impossible d'exécuter la requête dans la base de données", 'Les données en base ne correspondent pas à ce qui est attendu', 'impossible de générer un token Qgis', () => ({
          ...caminoError,
          status: HTTP_STATUS.INTERNAL_SERVER_ERROR,
        })),
        Match.exhaustive
      )
    )
  )
export const utilisateurs =
  (pool: Pool) =>
  async (
    { query }: { query: unknown },
    user: User
  ): Promise<{
    nom: string
    format: 'csv' | 'xlsx' | 'ods'
    contenu: string
  } | null> => {
    const searchParams = utilisateursSearchParamsValidator.and(z.object({ format: z.enum(['csv', 'xlsx', 'ods']).optional().default('csv') })).parse(query)

    const utilisateurs = await callAndExit(getUtilisateursFilteredAndSorted(pool, user, searchParams))

    const format = searchParams.format

    const entreprises = await callAndExit(getEntreprises(pool))
    const contenu = tableConvert('utilisateurs', utilisateursFormatTable(utilisateurs, entreprises), format)

    return contenu
      ? {
          nom: fileNameCreate(`utilisateurs-${utilisateurs.length}`, format),
          format,
          contenu,
        }
      : null
  }

type GetUtilisateursError = GetUtilisateursFilteredAndSortedErrors

export const getUtilisateurs: RestNewGetCall<'/rest/utilisateurs'> = (rootPipe): Effect.Effect<UtilisateursTable, CaminoApiError<GetUtilisateursError>> => {
  return rootPipe.pipe(
    Effect.bind('utilisateurs', ({ pool, user, searchParams }) => getUtilisateursFilteredAndSorted(pool, user, searchParams)),
    Effect.map(({ utilisateurs, searchParams }) => {
      return {
        elements: utilisateurs.slice((searchParams.page - 1) * searchParams.intervalle, searchParams.page * searchParams.intervalle),
        total: utilisateurs.length,
      }
    }),
    Effect.mapError(caminoError =>
      Match.value(caminoError.message).pipe(
        Match.whenOr("Impossible d'exécuter la requête dans la base de données", 'Les données en base ne correspondent pas à ce qui est attendu', () => ({
          ...caminoError,
          status: HTTP_STATUS.INTERNAL_SERVER_ERROR,
        })),
        Match.when('droits insuffisants', () => ({ ...caminoError, status: HTTP_STATUS.FORBIDDEN })),
        Match.when("L'utilisateur est invalide", () => ({ ...caminoError, status: HTTP_STATUS.BAD_REQUEST })),
        Match.exhaustive
      )
    )
  )
}

type GetUtilisateurError = GetUtilisateurByIdErrors | 'droits insuffisants'
export const getUtilisateur: RestNewGetCall<'/rest/utilisateurs/:id'> = (rootPipe): Effect.Effect<UserNotNull, CaminoApiError<GetUtilisateurError>> => {
  return rootPipe.pipe(
    Effect.flatMap(({ pool, params, user }) => newGetUtilisateurById(pool, params.id, user)),
    Effect.mapError(caminoError =>
      Match.value(caminoError.message).pipe(
        Match.whenOr("Impossible d'exécuter la requête dans la base de données", 'Les données en base ne correspondent pas à ce qui est attendu', () => ({
          ...caminoError,
          status: HTTP_STATUS.INTERNAL_SERVER_ERROR,
        })),
        Match.when('droits insuffisants', () => ({ ...caminoError, status: HTTP_STATUS.FORBIDDEN })),
        Match.when("L'utilisateur est invalide", () => ({ ...caminoError, status: HTTP_STATUS.BAD_REQUEST })),
        Match.exhaustive
      )
    )
  )
}
