const { callbackify } = require('util')
const OError = require('@overleaf/o-error')
const logger = require('@overleaf/logger')
const settings = require('@overleaf/settings')
const request = require('requestretry')
const { promisifyAll } = require('@overleaf/promise-utils')
const NotificationsBuilder = require('../Notifications/NotificationsBuilder')
const {
  V1ConnectionError,
  InvalidInstitutionalEmailError,
} = require('../Errors/Errors')
const { fetchJson, fetchNothing } = require('@overleaf/fetch-utils')

function _makeRequestOptions(options) {
  const requestOptions = {
    method: options.method,
    basicAuth: { user: settings.apis.v1.user, password: settings.apis.v1.pass },
    signal: AbortSignal.timeout(settings.apis.v1.timeout),
  }

  if (options.body) {
    requestOptions.json = options.body
  }

  return requestOptions
}

function _responseErrorHandling(options, error) {
  const status = error.response.status

  if (status >= 500) {
    throw new V1ConnectionError({
      message: 'error getting affiliations from v1',
      info: {
        status,
        body: error.body,
      },
    })
  }

  let errorBody

  try {
    if (error.body) {
      errorBody = JSON.parse(error.body)
    }
  } catch (e) {}

  let errorMessage
  if (errorBody?.errors) {
    errorMessage = `${status}: ${errorBody.errors}`
  } else {
    errorMessage = `${options.defaultErrorMessage}: ${status}`
  }

  throw new OError(errorMessage, { status })
}

async function _affiliationRequestFetchJson(options) {
  if (!settings.apis.v1.url) {
    return
  } // service is not configured

  const url = `${settings.apis.v1.url}${options.path}`

  const requestOptions = _makeRequestOptions(options)

  try {
    return await fetchJson(url, requestOptions)
  } catch (error) {
    _responseErrorHandling(options, error)
  }
}

async function _affiliationRequestFetchNothing(options) {
  if (!settings.apis.v1.url) {
    return
  } // service is not configured

  const url = `${settings.apis.v1.url}${options.path}`

  const requestOptions = _makeRequestOptions(options)

  try {
    await fetchNothing(url, requestOptions)
  } catch (error) {
    _responseErrorHandling(options, error)
  }
}

async function _affiliationRequestFetchNothing404Ok(options) {
  try {
    await _affiliationRequestFetchNothing(options)
  } catch (error) {
    const status = error.info?.status
    if (status !== 404) {
      throw error
    }
  }
}

function getInstitutionAffiliations(institutionId, callback) {
  makeAffiliationRequest(
    {
      method: 'GET',
      path: `/api/v2/institutions/${institutionId.toString()}/affiliations`,
      defaultErrorMessage: "Couldn't get institution affiliations",
    },
    (error, body) => callback(error, body || [])
  )
}

function getConfirmedInstitutionAffiliations(institutionId, callback) {
  makeAffiliationRequest(
    {
      method: 'GET',
      path: `/api/v2/institutions/${institutionId.toString()}/confirmed_affiliations`,
      defaultErrorMessage: "Couldn't get institution affiliations",
    },
    (error, body) => callback(error, body || [])
  )
}

function getInstitutionAffiliationsCounts(institutionId, callback) {
  makeAffiliationRequest(
    {
      method: 'GET',
      path: `/api/v2/institutions/${institutionId.toString()}/affiliations_counts`,
      defaultErrorMessage: "Couldn't get institution counts",
    },
    (error, body) => callback(error, body || [])
  )
}

function getLicencesForAnalytics(lag, queryDate, callback) {
  makeAffiliationRequest(
    {
      method: 'GET',
      path: `/api/v2/institutions/institutions_licences`,
      body: { query_date: queryDate, lag },
      defaultErrorMessage: 'Could not get institutions licences',
    },
    callback
  )
}

function getUserAffiliations(userId, callback) {
  makeAffiliationRequest(
    {
      method: 'GET',
      path: `/api/v2/users/${userId.toString()}/affiliations`,
      defaultErrorMessage: "Couldn't get user affiliations",
    },
    (error, body) => callback(error, body || [])
  )
}

async function getUsersNeedingReconfirmationsLapsedProcessed() {
  return await _affiliationRequestFetchJson({
    method: 'GET',
    path: '/api/v2/institutions/need_reconfirmation_lapsed_processed',
    defaultErrorMessage:
      'Could not get users that need reconfirmations lapsed processed',
  })
}

async function addAffiliation(userId, email, affiliationOptions) {
  const {
    university,
    department,
    role,
    confirmedAt,
    entitlement,
    rejectIfBlocklisted,
  } = affiliationOptions

  try {
    await _affiliationRequestFetchNothing({
      method: 'POST',
      path: `/api/v2/users/${userId.toString()}/affiliations`,
      body: {
        email,
        university,
        department,
        role,
        confirmedAt,
        entitlement,
        rejectIfBlocklisted,
      },
      defaultErrorMessage: "Couldn't create affiliation",
    })
  } catch (error) {
    if (error.info?.status === 422) {
      throw new InvalidInstitutionalEmailError(error.message).withCause(error)
    }
    throw error
  }

  if (!university) {
    return
  }

  // have notifications delete any ip matcher notifications for this university
  try {
    await NotificationsBuilder.promises
      .ipMatcherAffiliation(userId)
      .read(university.id)
  } catch (err) {
    // log and ignore error
    logger.err({ err }, 'Something went wrong marking ip notifications read')
  }
}

async function removeAffiliation(userId, email) {
  await _affiliationRequestFetchNothing404Ok({
    method: 'POST',
    path: `/api/v2/users/${userId.toString()}/affiliations/remove`,
    body: { email },
    defaultErrorMessage: "Couldn't remove affiliation",
  })
}

function endorseAffiliation(userId, email, role, department, callback) {
  makeAffiliationRequest(
    {
      method: 'POST',
      path: `/api/v2/users/${userId.toString()}/affiliations/endorse`,
      body: { email, role, department },
      defaultErrorMessage: "Couldn't endorse affiliation",
    },
    callback
  )
}

function deleteAffiliations(userId, callback) {
  makeAffiliationRequest(
    {
      method: 'DELETE',
      path: `/api/v2/users/${userId.toString()}/affiliations`,
      defaultErrorMessage: "Couldn't delete affiliations",
    },
    callback
  )
}

function addEntitlement(userId, email, callback) {
  makeAffiliationRequest(
    {
      method: 'POST',
      path: `/api/v2/users/${userId}/affiliations/add_entitlement`,
      body: { email },
      defaultErrorMessage: "Couldn't add entitlement",
    },
    callback
  )
}

function removeEntitlement(userId, email, callback) {
  makeAffiliationRequest(
    {
      method: 'POST',
      path: `/api/v2/users/${userId}/affiliations/remove_entitlement`,
      body: { email },
      defaultErrorMessage: "Couldn't remove entitlement",
      extraSuccessStatusCodes: [404],
    },
    callback
  )
}

function sendUsersWithReconfirmationsLapsedProcessed(users, callback) {
  makeAffiliationRequest(
    {
      method: 'POST',
      path: '/api/v2/institutions/reconfirmation_lapsed_processed',
      body: { users },
      defaultErrorMessage:
        'Could not update reconfirmation_lapsed_processed_at',
    },
    (error, body) => callback(error, body || [])
  )
}

const InstitutionsAPI = {
  getInstitutionAffiliations,

  getConfirmedInstitutionAffiliations,

  getInstitutionAffiliationsCounts,

  getLicencesForAnalytics,

  getUserAffiliations,

  getUsersNeedingReconfirmationsLapsedProcessed: callbackify(
    getUsersNeedingReconfirmationsLapsedProcessed
  ),

  addAffiliation: callbackify(addAffiliation),

  removeAffiliation: callbackify(removeAffiliation),

  endorseAffiliation,

  deleteAffiliations,

  addEntitlement,

  removeEntitlement,

  sendUsersWithReconfirmationsLapsedProcessed,
}

function makeAffiliationRequest(options, callback) {
  if (!settings.apis.v1.url) {
    return callback(null)
  } // service is not configured
  if (!options.extraSuccessStatusCodes) {
    options.extraSuccessStatusCodes = []
  }
  const requestOptions = {
    method: options.method,
    url: `${settings.apis.v1.url}${options.path}`,
    body: options.body,
    auth: { user: settings.apis.v1.user, pass: settings.apis.v1.pass },
    json: true,
    timeout: settings.apis.v1.timeout,
  }
  if (options.method === 'GET') {
    requestOptions.maxAttempts = 3
    requestOptions.retryDelay = 500
  } else {
    requestOptions.maxAttempts = 0
  }
  request(requestOptions, function (error, response, body) {
    if (error) {
      return callback(
        new V1ConnectionError('error getting affiliations from v1').withCause(
          error
        )
      )
    }
    if (response && response.statusCode >= 500) {
      return callback(
        new V1ConnectionError({
          message: 'error getting affiliations from v1',
          info: {
            status: response.statusCode,
            body,
          },
        })
      )
    }
    let isSuccess = response.statusCode >= 200 && response.statusCode < 300
    if (!isSuccess) {
      isSuccess = options.extraSuccessStatusCodes.includes(response.statusCode)
    }
    if (!isSuccess) {
      let errorMessage
      if (body && body.errors) {
        errorMessage = `${response.statusCode}: ${body.errors}`
      } else {
        errorMessage = `${options.defaultErrorMessage}: ${response.statusCode}`
      }

      logger.warn({ path: options.path, body: options.body }, errorMessage)
      return callback(
        new OError(errorMessage, { statusCode: response.statusCode })
      )
    }

    callback(null, body)
  })
}

InstitutionsAPI.promises = promisifyAll(InstitutionsAPI, {
  without: [
    'addAffiliation',
    'removeAffiliation',
    'getUsersNeedingReconfirmationsLapsedProcessed',
  ],
})

InstitutionsAPI.promises.addAffiliation = addAffiliation
InstitutionsAPI.promises.removeAffiliation = removeAffiliation
InstitutionsAPI.promises.getUsersNeedingReconfirmationsLapsedProcessed =
  getUsersNeedingReconfirmationsLapsedProcessed

module.exports = InstitutionsAPI
