import groovy.json.JsonSlurper
import groovy.xml.XmlSlurper
import org.codehaus.groovy.runtime.StackTraceUtils

import ch.nevis.idm.client.IdmRestClient
import ch.nevis.idm.client.IdmRestClientFactory


// AGOVaq conversion
def maxLoiRoleToCtxClssConvertorMap = [
    "level100": "urn:qa.agov.ch:names:tc:ac:classes:100",
    "level200": "urn:qa.agov.ch:names:tc:ac:classes:200",
    "level300": "urn:qa.agov.ch:names:tc:ac:classes:300",
    "level400": "urn:qa.agov.ch:names:tc:ac:classes:400",
    "level500": "urn:qa.agov.ch:names:tc:ac:classes:500"
]

// https://docs.nevis.net/nevisidm/Developer-Guide/SOAP-Interface/Interface-specification/Value-types#enum-value-types
def blockingCredentialStates = ['DISABLED', 'EXPIRED', 'LOCKED_TEMPORARY', 'LOCKED', 'ARCHIVED', 'RESET_CODE']

def getUserIdVerificationForRecovery(currentLoaRole) {
  // application is AGOV-AccountStatus
  def list = new XmlSlurper().parseText(session.get('ch.adnovum.nevisidm.userDto'))
  def result = list.'**'.find {node -> node.name() == 'properties' && node.name.text() == 'idVerification' && node.scopeName.text() == 'AGOV-AccountStatus,mustRecover'}?.value?.text()

  if (!result) {
    // fallback if not explicitly set
    def chDomicile = list.country.text() == 'ch'
    def lastIdVerification = list.'**'.find {node -> node.name() == 'properties' && node.name.text() == 'idVerification' && node.scopeName.text() == 'AGOV-Loi,' + currentLoaRole}?.value?.text() ?: 'missing'
    switch (currentLoaRole) {
      case 'level100':
        result = chDomicile ? 'SimpleLetter' : 'Video'
        break
      case 'level200':
        result = chDomicile ? 'Bmid' : 'Video'
        break
      case 'level300':
      case 'level400':
        result = chDomicile ? lastIdVerification : 'Video'
        break
      default:
        LOG.warn("unexpected loa on account: ${currentLoaRole}")
        // safest default, should work in any case
        result = 'Video'
    }
    LOG.warn("Recovery method not set, choosing ${result} (based on currentLoad: ${currentLoaRole}, CH-domicile: ${chDomicile}, last verification method: ${lastIdVerification})")
  }
  return result
}

def getAqLevelBasedOnIdVerificationForRecovery(idVerification, highestRoleLevel) {
  def result = 'level'

  switch (idVerification) {
    case 'None':
      result = result.concat('100')
      break
    case 'SimpleLetter':
      result = result.concat('200')
      break
    case 'Video':
    case 'VideoSelfPaid':
    case 'Bmid':
    case 'BmidSelfPaid':
    case 'Counter':
      result = result.concat((highestRoleLevel == 'level400') ? '400' : '300')
      break
    default:
      LOG.warn("unexpected idVerification for recovery on account: ${idVerification}")
      // safest default, should work in any case
      result = highestRoleLevel
  }

  return result
}

def getUserMustRecoverValidFrom() {
  // set attibutes from DTO: -> validFrom
  def payload = new XmlSlurper().parseText(session.get('ch.adnovum.nevisidm.userDto'))
  def authzNode = payload.'**'.find {node -> node.name() == 'authorizations' && node.role.name.text() == 'mustRecover'}
  return (authzNode) ? ((authzNode.validFrom && !authzNode.validFrom.text().isEmpty()) ? authzNode.validFrom?.text() : authzNode.ctlCreDat?.text()) : ''
}

def userHasNewLoginFactor() {
  IdmRestClient idmRestClient = IdmRestClientFactory.get(parameters)

  String baseUrl = parameters.get('baseUrl')
  String clientExtId = session.get('ch.adnovum.nevisidm.user.clientExtId')
  String userExtId = session.get('ch.adnovum.nevisidm.user.extId')
  String baseEndPoint = "$baseUrl/api/core/v1/$clientExtId/users/$userExtId"

  response.setSessionAttribute('agov.recovery.newLoginFactor', 'NONE')

  try {
     def credInfoArray = new JsonSlurper().parseText(idmRestClient.get("$baseEndPoint/generic-credentials"))

     def accessApp = credInfoArray['items'].find( it -> it.stateName == "active")
     if (accessApp) {
       response.setSessionAttribute('agov.recovery.accessapp', accessApp.properties.fidouaf_name)
       response.setSessionAttribute('agov.recovery.accessapp.dispatchTargetId', accessApp.identification.replaceAll('dispatch_target_', ''))
       response.setSessionAttribute('agov.recovery.newLoginFactor', 'ACCESS_APP')
       return true
     }

     credInfoArray = new JsonSlurper().parseText(idmRestClient.get("$baseEndPoint/fido2"))

     def fido2Key = credInfoArray['items'].find( it -> it.stateName == "active")
     if (fido2Key) {
       response.setSessionAttribute('agov.recovery.securityKey', fido2Key.userFriendlyName)
       response.setSessionAttribute('agov.recovery.newLoginFactor', 'FIDO2')
       return true
     }

  } catch(Exception e) {
    LOG.error(e.toString())
  }
  return false
}


// for autditing
def user = session['ch.adnovum.nevisidm.user.extId'] ?: 'unknown'
def sourceIp = request.getLoginContext()['connection.HttpHeader.X-Real-IP'] ?: 'unknown'
def userAgent = request.getLoginContext()['connection.HttpHeader.user-agent'] ?: request.getLoginContext()['connection.HttpHeader.User-Agent'] ?: 'unknown'
def maxLoi = null


// new
if (session['ch.adnovum.nevisidm.userDto'] != null && notes['lasterror'] == null) {
  try {
    def userDto = new XmlSlurper().parseText(session['ch.adnovum.nevisidm.userDto'])
    def userState = userDto.state
    def recoveryCode = userDto.'**'.find {node -> node.name() == 'credentials' && node.type.text() == 'CONTEXT_PASSWORD' && node.context.text() == 'RECOVERY'}

    LOG.debug("Recovery: Dto is '${userDto}")
    LOG.debug("Recovery: state is '${userState}")
    LOG.debug("Recovery: RecoveryCode is '${recoveryCode ? recoveryCode : 'none'}'") 

    if (userState == 'ACTIVE') {

      response.setSessionAttribute('agov.recovery.authnContextClassRef', 'urn:qa.agov.ch:names:tc:ac:classes:recovery')
      response.setSessionAttribute('agov.recovery.authenticatedWith', 'urn:qa.agov.ch:names:tc:authfactor:email')
      response.setSessionAttribute('agov.recovery.codeStatus', 'notNeeded')
      response.setSessionAttribute('agov.recovery.codeDetailStatus', 'n/a')
      response.setSessionAttribute('agov.recovery.newLoginFactor', 'NONE')

      def maxLoiList =   userDto.'**'.findAll { node -> node.name() == 'roles' && node.applicationName.text() == 'AGOV-Loi' }.collect({ node -> node.name.text() })
      maxLoi = (maxLoiList == null || maxLoiList.isEmpty()) ? null : maxLoiList.sort().last()

      def idVerification = null
      def agovAqValidFrom = null
      if (maxLoi) {
        if (maxLoi != 'level100') {
          response.setSessionAttribute('agov.recovery.codeDetailStatus', '' + maxLoi)
        }

        idVerification = userDto.'**'.find { node -> node.name() == 'properties' && node.name.text() == 'idVerification' && node.scopeName.text() == 'AGOV-Loi,' + maxLoi}?.value?.text()
        idVerification = idVerification ?: 'None'
        agovAqValidFrom = userDto.'**'.find { node -> node.name() == 'authorizations' && node.role.name.text() == maxLoi}?.validFrom?.text()
        agovAqValidFrom = agovAqValidFrom?: userDto.'**'.find { node -> node.name() == 'authorizations' && node.role.name.text() == maxLoi}?.ctlCreDat?.text()
      }

      def mustRecover =   userDto.'**'.find { node -> node.name() == 'roles' && node.applicationName.text() == 'AGOV-AccountStatus' && node.name.text() == 'mustRecover' }

      def hasRecoveryRole = userDto.'**'.find { node -> node.name() == 'roles' && node.applicationName.text() == 'AGOV-AccountStatus' && node.name.text() == 'recovery' }

      def hasRecoveryCascadeRole = userDto.'**'.find { node -> node.name() == 'roles' && node.applicationName.text() == 'AGOV-AccountStatus' && node.name.text() == 'recoveryCascade' }

      def hasNewLoginFactor = hasRecoveryRole && userHasNewLoginFactor()

      if (mustRecover) {
        // attributes are defined over the mustRecover authorization
        response.setSessionAttribute('agov.recovery.authnContextClassRef', 'urn:qa.agov.ch:names:tc:ac:classes:mustRecover')
        response.setSessionAttribute('agov.recovery.codeDetailStatus', 'mustRecover')

        idVerification = getUserIdVerificationForRecovery(maxLoi ?: 'level100') ?: idVerification

        agovAqValidFrom = getUserMustRecoverValidFrom()

        maxLoi = getAqLevelBasedOnIdVerificationForRecovery(idVerification, maxLoi)
      } else if (hasRecoveryCascadeRole && hasNewLoginFactor) {
        response.setSessionAttribute('agov.recovery.authnContextClassRef', 'urn:qa.agov.ch:names:tc:ac:classes:recoveryCascade')
      }

      LOG.debug("Recovery: MaxLoi is '${maxLoi}'")
      LOG.debug("Recovery: IdVerification is  ${idVerification}")
      LOG.debug("Recovery: agovAqValidFrom is ${agovAqValidFrom}")
      LOG.debug("Recovery: mustRecover is '${mustRecover}'")
      LOG.debug("Recovery: hasRecoveryRole is '${hasRecoveryRole}'")
      LOG.debug("Recovery: hasNewLoginFactor is '${hasNewLoginFactor}'")

      if (maxLoi != null) {       
        if (maxLoiRoleToCtxClssConvertorMap.containsKey(maxLoi)) {
          LOG.debug("Recovery: MaxLoiMapping is " + maxLoiRoleToCtxClssConvertorMap[maxLoi])
          response.setSessionAttribute('agov.recovery.currentAgovAq', '' + maxLoiRoleToCtxClssConvertorMap[maxLoi])
          response.setSessionAttribute('agov.recovery.currentIdVerification', '' + idVerification)
          response.setSessionAttribute('agov.recovery.currentAgovAqRoleValidFrom', '' + agovAqValidFrom)

          if ((maxLoi == 'level100') && (mustRecover == null) && !hasNewLoginFactor) {
            // AQ100 accounts need to use the recovery code, if they can
            // check the status of recoveryCode credential
            if (recoveryCode && !blockingCredentialStates.contains(recoveryCode.state.text())) {
              LOG.debug("Recovery: emailAndCode")
              response.setResult('needCode')
              return
            } else {
              LOG.warn("AGOVaq100 recovery: skipped Recovery-Code check '${recoveryCode ? recoveryCode.state.text() : 'MISSING'}'")
              response.setSessionAttribute('agov.recovery.codeStatus', 'skipped')
              response.setSessionAttribute('agov.recovery.codeDetailStatus', "unusable (state: ${recoveryCode ? recoveryCode.state.text() : 'MISSING'})")
              response.setResult('ok')
              return
            }

          } else if ((maxLoi == 'level100') && hasNewLoginFactor) {
            LOG.debug("Recovery: new Login Factor")
            response.setSessionAttribute('agov.recovery.codeStatus', 'skipped')
            response.setSessionAttribute('agov.recovery.codeDetailStatus', "new login factor already registered (${session['agov.recovery.newLoginFactor']})")
            response.setResult('ok')
            return

          } else {
            LOG.debug("Recovery: email")
            response.setResult('ok')
            return
          }

        } else {
          LOG.error("Recovery: Failed to convert '${maxLoi}' to AGOVaq")
          response.setResult('error')
          return
        }
      } else {
        // maxLoi is null
        LOG.debug("Recovery: no 'AGOV-Loi'-role assigned to user ${user}")
        if ((hasRecoveryRole != null) && (mustRecover == null)) {
          response.setResult('notFullyRegistered')
          return
        } else {
          LOG.error("Recovery: no 'AGOV-Loi'-role assigned to user ${user} and no recovery role ")
          response.setResult('error')
          return
        }
      }
    } else {
      // state != ACTIVE and no lasterror should not happen
      LOG.error("Recovery: state='${userState}' but not lasterror set")
      response.setNote('lasterror', '9909')
      response.setNote('lasterrorinfo', 'internal error')
      response.setResult('error')
      return
    }
  } catch (Exception e) {
    e = StackTraceUtils.sanitize(e)
    def affectedLines = e.stackTrace.findAll { it.className.startsWith('Script') }.collect { "${it.methodName}:${it.lineNumber}" }
    LOG.error("FATAL: Recovery processing failed (at lines: ${affectedLines})", e)
    response.setNote('lasterror', '9909')
    response.setNote('lasterrorinfo', 'internal error')
    response.setResult('error')
    return
  }
}

LOG.error("Recovery: userDto missing or failure before (lasterror='${notes.getProperty('lasterror', '-')}')")
response.setNote('lasterror', '9909')
response.setNote('lasterrorinfo', 'internal error')
response.setResult('error')
return
