const cookie = require('cookie')
const UB = require('@unitybase/ub')
const App = UB.App
const oidcConst = require('./oidcConst')
const { OpenIdProvider } = require('./openIdProvider')
/**
 * OpenIDConnect client for UnityBase
 *
 const openID = require('@unitybase/openid-connect')
 let oIdEndPoint = openID.registerEndpoint('openIDConnect')
 oIdEndPoint.registerProvider('Google', {
        authUrl: 'https://accounts.google.com/o/oauth2/auth',
        tokenUrl: 'https://accounts.google.com/o/oauth2/token',
        userInfoUrl: 'https://www.googleapis.com/oauth2/v1/userinfo',
        userInfoHTTPMethod: 'GET',
        scope: 'openid',
        nonce: '123',
        response_type: 'code',
        client_id: '350085411136-lpj0qvr87ce0r0ae0a3imcm25joj2t2o.apps.googleusercontent.com',
        client_secret: 'dF4qmUxhHoBAj-E1R8YZUCqA',
        // getOnFinishAction: function (response) {
        //   return 'opener.$App.onFinishOpenIDAuth(' + JSON.stringify(response) + '); close();'
        // },
        getUserID: function(userInfo) {
          let uID = UB.Repository('uba_user').attrs(['ID'])
             .where('[name]', '=', userInfo.id).selectScalar()
          return uID || null
        }
      })
 *
 * @module @unitybase/openid-connect
 * @tutorial security_openidconnect
 */

const endpoints = {}
const providers = {}

module.exports.registerEndpoint = registerOpenIDEndpoint
module.exports.registerProvider = registerOpenIDProvider
module.exports.provider = provider

/**
 * Return provider by name
 * @param {string} providerName
 * @returns {OpenIdProvider|undefined}
 */
function provider (providerName) {
  return providers[providerName]
}

/**
 * OpenID endpoint. Able to register providers
 * @typedef {object} openIDEndpoint
 * @property {Function} registerProvider
 */

/**
 * @typedef {object} ProviderConfig
 * @property {string} providerConfig.name
 * @property {string} providerConfig.authUrl Provider's authorisation url
 * @property {string} providerConfig.tokenUrl Provider's token url
 * @property {string} providerConfig.userInfoUrl provider's userinfo url
 * @property {string} [providerConfig.logoutUrl] Logout url
 * @property {string} [providerConfig.userInfoHTTPMethod='GET'] Http method for userinfo request. Default GET
 * @property {string} providerConfig.scope Requested scopes delimited by '+' symbol
 * @property {string} [providerConfig.resource] Requested resource (required for MS ADFS3 windows server 2012)
 * @property {string} [providerConfig.nonce] nonce  TODO - generate random and cache in GlobalCache with expire
 * @property {string} providerConfig.response_type response type. Must contain code. This module use code responce type.
 * @property {string} providerConfig.client_id client_id. Get it from provider
 * @property {string} [providerConfig.client_secret] client_secret. Get it from provider (not needed for ADFS3 - windows server 2012)
 * @property {string} [providerConfig.cert] id.gov.ua specific - a BASE64 key distribution protocol certificate
 *   to which the response will be encrypted by the authentication server. see https://id.gov.ua/downloads/IDInfoProcessingD.pdf
 * @property {string} [providerConfig.fields] id.gov.ua specific - comma separated names of the requested user certificate fields.
 *  If the names of the fields are not specified, then all available certificate fields are returned
 *  with information about the user (certificate owner) and the issuer during call to `userInfoUrl` provider endpoint
 * @property {Function} providerConfig.getCustomFABody Function, that returns custom text included to final html success/fail response
 * @property {Function} [providerConfig.getAuthCustomHeaders]
 * @property {string} [providerConfig.response_mode='form_post'] One of: form_post, fragment, query
 * @property {Function} providerConfig.getOnFinishAction Function, that returns client-side code to be run after success/fail response from OpenID provider.
 *  For example: `opener.$App.onFinishOpenIDAuth`. In case of success will be called with `{success: true, data: userData, secretWord: secretWord}`
 *  In case of fail `{success: false}`
 * @property {Function} providerConfig.getUserID Called with one 1 parameter - provider's response for userInfo request. Must return user ID from uba_user entity if user is authorised or null else
 * @memberOf openIDEndpoint
 */

/**
 * Register openID connect endpoint. In case endpoint already registered - return existed
 *
 * @function
 * @param {string} endpointName
 * @returns {openIDEndpoint} endpoint
 */
function registerOpenIDEndpoint (endpointName) {
  if (endpoints[endpointName]) {
    return endpoints[endpointName]
  }

  App.registerEndpoint(endpointName, openIDConnectEp, false)

  endpoints[endpointName] = {
    /**
     * Register OpenID provider
     * @param {string} name
     * @param {ProviderConfig} providerConfig
     */
    registerProvider: function (name, providerConfig) {
      registerOpenIDProvider(name, providerConfig)
    },
    getProviderList: function () {
      return Object.keys(providers)
    },
    /**
     * Get provider by name
     * @param {string} providerName
     * @returns {OpenIdProvider|null}
     */
    getProvider: function (providerName) {
      return provider(providerName)
    }
  }
  return endpoints[endpointName]
}

/**
 * Register OpenID provider
 * @param {string} name
 * @param {ProviderConfig} providerConfig
 */
function registerOpenIDProvider (name, providerConfig) {
  if (providers[name]) {
    console.info(`OIDC: provider ${name} already registered`)
  } else {
    providers[name] = new OpenIdProvider(name, providerConfig)
  }
}

/**
 * OpenID `Authorization Code Flow` (user interaction) endpoint implementation
 *  - https://tools.ietf.org/html/rfc6749#section-4.1
 *  - https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow
 *
 * If called as /endpoint - return a list of registered openIDConnect providers,
 * If called as /endpoint/provider:
 *   - without parameters  or with mode === 'auth' - redirect to provider `authUrl`
 *   - with parameters `code` and `state` - call doProviderAuthHandshake method
 *   - with parameters `logout` - redirect to log out url
 *
 * @param {THTTPRequest} req
 * @param {THTTPResponse} resp
 * @protected
 */
function openIDConnectEp (req, resp) {
  const providerName = req.uri
  const url = req.url.split('?')[0]
  const endpointUrl = providerName ? url.substr(0, url.length - providerName.length - 1) : url
  const endpointName = endpointUrl.substr(endpointUrl.lastIndexOf('/') - endpointUrl.length + 1)
  const endpoint = endpoints[endpointName]

  if (!endpoint) {
    return resp.badRequest(`OpenID endpoint '${endpointName}' is not registered`)
  }
  if (!providerName) {
    resp.statusCode = 200
    resp.writeEnd(JSON.stringify(endpoint.getProviderList()))
    return
  }

  const provider = endpoint.getProvider(providerName)
  if (!provider) {
    return resp.badRequest(`OpenID provider '${providerName}' is not registered`)
  }

  const redirectUrl = App.externalURL + (App.externalURL.endsWith('/') ? '' : '/') + endpointName + '/' + providerName
  const params = (req.method === 'GET') ? req.parsedParameters : req.json()

  if (!Object.keys(params).length || params.mode === 'auth') {
    provider.redirectToProviderAuth(req, resp, redirectUrl, params)
  } else if (params.code && params.state) {
    const origin = req.getHeader('origin') || ''
    // validate session ID from cookies match state
    const cookiesStr = req.getHeader('Cookie') || ''
    console.debug('Cookie header value:', cookiesStr)
    const cookies = cookie.parse(cookiesStr)
    const ssid = cookies[oidcConst.OIDC_SESSID_COOKIE]
    let err = ''
    let ssidKey
    if (!ssid) {
      // App.globalCachePut(OIDC_SESSID_COOKIE + randomSid, randomState)
      // resp.writeHead(`Set-Cookie: ${OIDC_SESSID_COOKIE}=${randomSid}; Secure; HttpOnly`)
      err = `OIDC: Cookie ${oidcConst.OIDC_SESSID_COOKIE} is required`
    } else {
      ssidKey = oidcConst.OIDC_SESSID_COOKIE + ssid
      const sessionState = App.globalCacheGet(ssidKey)
      if (!sessionState) {
        err = `OIDC: state for ssid ${ssidKey} is not found`
      } else if (sessionState !== params.state) {
        err = `OIDC: initial state for ssid ${ssidKey} is ${sessionState} but got ${params.state}`
      }
    }
    if (err) {
      provider.notifyProviderError(resp, err)
    } else {
      App.globalCachePut(ssidKey, null) // remove key
      provider.doProviderAuthHandshake(resp, params.code, params.state, redirectUrl, origin)
    }
  } else if (params.logout) {
    provider.redirectToProviderLogout(req, resp, params)
  } else {
    provider.redirectToProviderAuth(req, resp, redirectUrl, params)
  }
}