const joi = require('joi')
const url = require('url')
const querystring = require('querystring')
const got = require('got')
const config = require('src/config/index.js')

const mod = {}
module.exports = mod

/**
 * @return {string}
 */
mod.getAuthURL = (state) => {
  const input = joi
    .object({
      authorized_endpoint: joi.string().required(),
      token_endpoint: joi.string().required(),
      client_id: joi.string().required(),
      client_secret: joi.string().required(),
      state: joi.string().allow('', null).default('')
    })
    .unknown()
    .validate({ ...config.sso, state })
  if (input.error) throw new Error(input.error.message)

  /**
   * @type {{value: { authorized_endpoint: string, token_endpoint: string, client_id: string, client_secret: string, state: string }}}
   */
  const { value } = input

  const redirectUri = new url.URL('/oauth/redirect', config.server.url)

  const qs = {
    client_id: value.client_id,
    scope: 'offline_access',
    response_type: 'code',
    redirect_uri: redirectUri.toString()
  }
  if (value.state) qs.state = state

  return `${value.authorized_endpoint}?${querystring.stringify(qs)}`
}

/**
 * @return {string}
 */
mod.getLogoutURL = () => {
  const input = joi
    .object({
      logout_endpoint: joi.string().required()
    })
    .unknown()
    .validate({ ...config.sso })
  if (input.error) throw new Error(input.error.message)
  const redirectUri = new url.URL('/oauth/redirect', config.server.url)

  const qs = { state: 'logout', redirect_uri: redirectUri.toString() }

  return `${input.value.logout_endpoint}?${querystring.stringify(qs)}`
}

/**
 * @param {string} token
 * @return {Promise<{username: string, display_name: string, email: string, groups: string[]}>}
 */
mod.getUserInfo = async (token) => {
  /**
   * @type {{
   *  client_id: string,
   *  userinfo_endpoint: string,
   *  token: string,
   * }}
   */
  const input = await joi
    .object({
      client_id: joi.string().required(),
      client_secret: joi.string().required(),
      userinfo_endpoint: joi.string().required(),
      token: joi.string().required()
    })
    .unknown()
    .validateAsync({ ...config.sso, token })

  try {
    const resp = await got.default.get(input.userinfo_endpoint, {
      responseType: 'json',
      headers: { Authorization: `Bearer ${input.token}` }
    })

    const body = await joi.object({
      name: joi.string(),
      email: joi.string().email().required(),
      groups: joi.array().items(joi.string()).default([]),
      family_name: joi.string(),
      given_name: joi.string(),
      preferred_username: joi.string()
    }).unknown().validateAsync(resp.body)

    const displayName = body.name || body.preferred_username || body.given_name || ''
    return { display_name: displayName, email: body.email, groups: body.groups, username: body.preferred_username || '' }
  } catch (err) {
    console.log(err)
    if (err instanceof got.HTTPError) {
      if (err.code === 401) {
        // try refresh token
        return null
      }
    }
    throw err
  }
}

mod.refreshToken = async (token) => {
  /**
   * @type {{
   *  token_endpoint: string,
   *  client_id: string,
   *  client_secret: string,
   *  token: string,
   * }}
   */
  const input = await joi.object({
    token_endpoint: joi.string().required(),
    client_id: joi.string().required(),
    client_secret: joi.string().required(),
    token: joi.string().required()
  }).unknown().validateAsync({ ...config.sso, token })

  const qs = {
    client_id: input.client_id,
    client_secret: input.client_secret,
    grant_type: 'refresh_token',
    refresh_token: input.token
  }

  const resp = await got.default.post(input.token_endpoint, {
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    body: querystring.stringify(qs),
    responseType: 'json'
  })

  const body = await joi.object({
    access_token: joi.string().required(),
    refresh_token: joi.string().required()
  }).unknown().validateAsync(resp.body)

  return { access_token: body.access_token, refresh_token: body.refresh_token }
}

/**
 * @typedef SSOAccount
 * @property {string} access_token
 * @property {string} refresh_token
 * @property {string} username
 * @property {string} display_name
 * @property {string} email
 * @property {string[]} groups
 */

mod.getToken = async (code, state) => {
  const input = joi
    .object({
      authorized_endpoint: joi.string().required(),
      token_endpoint: joi.string().required(),
      client_id: joi.string().required(),
      client_secret: joi.string().required(),
      state: joi.string().required(),
      code: joi.string().required()
    })
    .unknown()
    .validate({ ...config.sso, state, code })

  if (input.error) throw new Error(input.error.message)

  /**
   * @type {{value: { authorized_endpoint: string, token_endpoint: string, client_id: string, client_secret: string, state: string, code: string }}}
   */
  const { value } = input

  const redirectUri = new url.URL('/oauth/redirect', config.server.url)

  const qs = {
    client_id: value.client_id,
    client_secret: value.client_secret,
    redirect_uri: redirectUri.toString(),
    code: value.code,
    client_session_state: value.state,
    grant_type: 'authorization_code'
  }

  const resp = await got.default.post(value.token_endpoint, {
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    body: querystring.stringify(qs),
    responseType: 'json'
  })

  const { body } = resp
  if (!body) throw new Error('resopnse body empty')

  const {
    access_token: accessToken,
    refresh_token: refreshToken
  } = body

  const userInfo = await mod.getUserInfo(accessToken)
  if (!userInfo) throw new Error('user info get fail')

  // decode access token
  console.log('user info ::: ', userInfo)

  console.log('body ::: ', body)

  /** @type {SSOAccount} */
  const ssoAccount = {
    access_token: accessToken,
    refresh_token: refreshToken,
    ...userInfo
  }

  return ssoAccount
}
