import ch.nevis.esauth.auth.engine.AuthResponse
import ch.nevis.esauth.sess.Session
import ch.nevis.esauth.util.httpclient.api.HttpClient
import groovy.json.JsonSlurper
import io.opentelemetry.api.trace.Span

import java.time.LocalDate
import java.time.ZoneId
import java.time.ZoneOffset

import com.fasterxml.uuid.Generators

def getHeader(String name) {
	def inctx = request.getLoginContext()
	// case-insensitive lookup of HTTP headers
	def map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER)
	map.putAll(inctx)
	return map['connection.HttpHeader.' + name]
}

// returns true on success and false on failure
def getNewVerification(Session sess, HttpClient httpClient, String verification_request_template, String traceparent){
	// Initialize the verification session on the verifier
	def endPoint = "${parameters.get('eidVerifierBaseUrl')}/api/v1/verifications"

	try {
		def httpResponse = Http.post()
				.url(endPoint)
				.header("Accept", "application/json")
				.header("traceparent", traceparent)
				.entity(Http.entity()
						.content(verification_request_template.replaceAll("\\{\\{UUID}}", UUID.randomUUID().toString()))
						.contentType("application/json")
						.build())
				.build()
				.send(httpClient)


		if (httpResponse.code() != 200) {
			LOG.debug("Result: ${httpResponse}")
			return false
		}

		def json = new JsonSlurper().parseText(httpResponse.bodyAsString())
		LOG.debug("Result: ${json}")

		sess.setAttribute('agov.eid.verification', 'true')
		sess.setAttribute('agov.eid.verification.id', json.id)
		sess.setAttribute('agov.eid.verification.link', json.verification_url)


        //  TODO/aca/2025-04-04:This could probably also be INITIATED, once the verifier supports this status
		if (json.state != 'PENDING') {
			return false
		}
	}
	catch (Exception e) {
		LOG.error("Eid verification failed: $e")
		return false
	}
    return true
}

def clearEidSession(){
    def s = request.getAuthSession(true)
    s.removeAttribute('agov.eid.verification')
    s.removeAttribute('agov.eid.verification.id')
    s.removeAttribute('agov.eid.verification.link')
}

def verification_request_template = '''
{ "presentation_definition": {
    "id": "{{UUID}}",
    "name": "AGOV Verification",
    "purpose": "AGOV Login",
    "format": {
      "vc+sd-jwt": {
        "sd-jwt_alg_values": [
          "ES256"
        ],
        "kb-jwt_alg_values": [
          "ES256"
        ]
      }
    },
    "input_descriptors": [
      {
        "id": "agov-all-attributes",
        "name": "AGOV Identity Verification",
        "purpose": "verification and authentication",
        "format": {
          "vc+sd-jwt": {
            "sd-jwt_alg_values": [
              "ES256"
            ],
            "kb-jwt_alg_values": [
              "ES256"
            ]
          }
        },
        "constraints": {
          "fields": [
            {
              "path": [
                "$.family_name"
              ]
            },
            {
              "path": [
                "$.given_name"
              ]
            },
            {
              "path": [
                "$.birth_date"
              ]
            },
            {
              "path": [
                "$.sex"
              ]
            },
            {
              "path": [
                "$.place_of_origin"
              ]
            },
            {
              "path": [
                "$.birth_place"
              ]
            },
            {
              "path": [
                "$.nationality"
              ]
            },
            {
              "path": [
                "$.personal_administrative_number"
              ]
            },
            {
              "path": [
                "$.document_number"
              ]
            },
            {
              "path": [
                "$.issuance_date"
              ]
            },
            {
              "path": [
                "$.expiry_date"
              ]
            },
            {
              "path": [
                "$.issuing_authority"
              ]
            },
            {
              "path": [
                "$.issuing_country"
              ]
            }
          ]
        }
      }
    ]
  }
}
'''

def ERROR_CODE_TO_STATUS_MAPPER = [
		'CREDENTIAL_INVALID'               : 'FAILED',
		'JWT_EXPIRED'                      : 'ERROR',
		'INVALID_FORMAT'                   : 'ERROR',
		'CREDENTIAL_EXPIRED'               : 'FAILED',
		'MISSING_NONCE'                    : 'ERROR',
		'UNSUPPORTED_FORMAT'               : 'ERROR',
		'CREDENTIAL_REVOKED'               : 'FAILED',
		'CREDENTIAL_SUSPENDED'             : 'FAILED',
		'HOLDER_BINDING_MISMATCH'          : 'ERROR',
		'CREDENTIAL_MISSING_DATA'          : 'FAILED',
		'UNRESOLVABLE_STATUS_LIST'         : 'ERROR',
		'PUBLIC_KEY_OF_ISSUER_UNRESOLVABLE': 'ERROR',
		'CLIENT_REJECTED'                  : 'CANCELED',
		'ISSUER_NOT_ACCEPTED'              : 'ERROR'
]

// ---------------
// check, whether we are still processing the correct AuthnRequest
// or if the frontend requested a timeout
if ( (inargs.containsKey('authRequestId') && (inargs['authRequestId'] != session['ch.nevis.auth.saml.request.id'])) || inargs['oid4vp'] == 'TIMEOUT') {
	// wrong request, "force" a timeout
	LOG.debug('authentication timeout enforced, due to concurrent requests (authRequestId missmatch) -> return a 408')

	response.setIsDirectResponse(true)
	response.setContentType('text/html; charset=UTF-8')
	response.setContent('Timeout')
	response.setHttpStatusCode(205)
	response.setHeader('IDP-AUTH', 'Timeout')

	// CONTINUE to keep the other request beeing processed
	response.setStatus(AuthResponse.AUTH_CONTINUE)
	return
}

def sess = request.getAuthSession(true)

if (inargs['oid4vp'] == 'ERROR') {
    LOG.debug("oid4vp error")
	response.setResult('error')
	return
}

if (inargs['oid4vp'] == 'SUCCEEDED') {
    LOG.debug("oid4vp succeeded")
	response.setResult('ok')
	return
}

// switch to access App
if (inargs['accessApp'] == 'accessApp') {
    //TODO/aca/2025/06/19: In theory we could also land here when we send 'SUCCESS' to the frontend -> would be better to  clear all session vaiables that can be set in this Authstate
    //TODO/aca/2025/06/19: Should we here rather set the LOGINMETHOD cookie and send an error assertion, since otherwise we might swich states too often and Nevis will kill the session?
    clearEidSession()
    LOG.debug("Switch to Access App")
    sess.setAttribute('agov.lastLoginMethod', 'accessApp')
	response.setResult('agovLogin')
	return
}

// switch to fido2
if (inargs['securityKey'] == 'securityKey') {
    clearEidSession()
    LOG.debug("Switch to Security Key")
    sess.setAttribute('agov.lastLoginMethod', 'securityKey')
	response.setResult('agovLogin')
	return
}

// switch to registration
if (inargs['fallback'] == 'register') {
    clearEidSession()
    LOG.debug("Switch to registration")
	response.setResult('register')
	return
}


HttpClient httpClient = HttpClients.create(parameters)
def spanCtxt = Span.current().getSpanContext()
def traceparent = "00-${spanCtxt.getTraceId()}-${spanCtxt.getSpanId()}-${spanCtxt.getTraceFlags().asHex()}"


if (getHeader('Content-Type') == 'application/json' && inargs.containsKey('o.id.v')) {
    LOG.debug("Request Status Update")
	// request for a status update from the verifier
	def result

    // FE requested a new verification
    if (inargs['o.id.v'] == 'NEW' || inargs['o.id.v'] == 'RESET') {
        LOG.debug("Initializing new verification")
	    if(!getNewVerification(sess, httpClient, verification_request_template, traceparent)){
            response.setResult('error') 
            return
        }
    }

	def idvalue = (!inargs['o.id.v'] || inargs['o.id.v'] == 'NEW' || inargs['o.id.v'] == 'RESET') ? session['agov.eid.verification.id'] : inargs['o.id.v']

    LOG.error("IDValSent: " + idvalue)

    // check, whether we are still processing the same verification request or if a new one was generated in e.g. another Tab
    if(inargs['o.id.v'] && inargs['o.id.v'] != 'NEW' && inargs['o.id.v'] != 'RESET' && inargs['o.id.v'] != session['agov.eid.verification.id']){
        // wrong request, tell fe to stop polling and request a timeout
        LOG.debug('authentication timeout enforced, due to concurrent requests (verificationRequest missmatch) -> Notify FE & then return a 408')
        result = """{
				"oid4vp": {
					"status": "TIMEOUT",
					"verification_url": "${session['agov.eid.verification.link']}",
					"id": "${idvalue}",
					"error_code": "REQUEST-MISMATCH",
					"error_message": "Request Mismatch Detected: Forcing Timeout"
				}}"""

        response.setContent(result.toString())
	    response.setContentType('application/json')
	    response.setHttpStatusCode(200)
	    response.setIsDirectResponse(true)
	    response.setStatus(AuthResponse.AUTH_CONTINUE)
	    return
    }

	try {
		def endPoint = "${parameters.get('eidVerifierBaseUrl')}/api/v1/verifications/${idvalue}"

		def httpResponse = Http.get()
				.url(endPoint)
				.header("Accept", "application/json")
				.header("traceparent", traceparent)
				.build()
				.send(httpClient)


        // 404 -> request a new verification
        if(httpResponse.code() == 404){
            // Frontend should know that we are starting a new request and not recieve an error
            def status = "FAILED"
            // Delete session variable to start a new verification
            sess.removeAttribute('agov.eid.verification')

            result = """{
				"oid4vp": {
					"status": "${status}",
					"verification_url": "",
					"id": "",
					"error_code": "HTTP-ERROR",
					"error_message": "Faild to verify status of verification, http status: ${httpResponse.code()}"
				}}"""
			LOG.warn("<== Response: ${httpResponse.code()}")
		}
		else if (httpResponse.code() != 200) {
			LOG.debug("Result: ${httpResponse}")
            
            def status = "ERROR"
            
			result = """{
				"oid4vp": {
					"status": "${status}",
					"verification_url": "${session['agov.eid.verification.link']}",
					"id": "${idvalue}",
					"error_code": "HTTP-ERROR",
					"error_message": "failed to verify status of verification ${idvalue}, http status: ${httpResponse.code()}"
				}}"""
			LOG.warn("<== Response: ${httpResponse.code()}")
		}
		else {

			def json = new JsonSlurper().parseText(httpResponse.bodyAsString())
            LOG.debug(httpResponse.bodyAsString())
			if (json.state == 'SUCCESS') {
				def claims = json.wallet_response.credential_subject_data
                LOG.debug("Store user data in session")
				
                def validFrom = LocalDate.parse(claims.issuance_date, DateTimeFormatter.ISO_LOCAL_DATE).atStartOfDay(ZoneId.systemDefault()).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)
                def validTo = LocalDate.parse(claims.expiry_date, DateTimeFormatter.ISO_LOCAL_DATE).atTime(23,59,59).atOffset(ZoneOffset.systemDefault()).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)

                sess.setAttribute('agov.eid.User.firstName', claims.given_name)
				sess.setAttribute('agov.eid.User.lastName', claims.family_name)
				sess.setAttribute('agov.eid.User.birthDate', claims.birth_date)
				sess.setAttribute('agov.eid.User.gender', claims.sex)
				sess.setAttribute('agov.eid.User.svnr', claims.personal_administrative_number.replace('.',''))
				sess.setAttribute('agov.eid.User.placeOfBirth', claims.birth_place)
				sess.setAttribute('agov.eid.User.eIdNumber', claims.document_number)
                // Simpler for later comparison -> Is converted again to upper case in the saml assertion
				sess.setAttribute('agov.eid.User.nationality', claims.nationality.toString().toLowerCase())

				sess.setAttribute('ValidFrom', validFrom)
				sess.setAttribute('ValidTo', validTo)
				sess.setAttribute('authenticatedWith', "urn:qa.agov.ch:names:tc:authfactor:eid")
				sess.setAttribute('idVerification', "Eid")

                // BUNDBITBK-5203 Dynamic aq levels
                def requestedRoleLevel = session['agov.requestedRoleLevel']
                if(requestedRoleLevel == "600"){
                    sess.setAttribute('contextClassRefToSet', "urn:qa.agov.ch:names:tc:ac:classes:600")
                }else{
                    sess.setAttribute('contextClassRefToSet', "urn:qa.agov.ch:names:tc:ac:classes:500")
                }

                // subjectUUID v5
                def namespace = UUID.fromString(parameters.get('eidUUIDNamespace'))
                def uuid = Generators.nameBasedGenerator(namespace).generate(claims.personal_administrative_number)
                 LOG.debug("UUID derived from svnr: ${uuid}")
                String uuidString = uuid.toString()
                sess.setAttribute('agov.subjectUUID', '' + uuidString)

				response.setUserId(uuidString)
                sess.setAttribute('ch.adnovum.nevisidm.user.extId', uuidString)
				response.setLoginId(claims.document_number)
				response.setAuthLevel("EID")

				result = """{
				"oid4vp": {
					"status": "SUCCEEDED",
					"verification_url": "${session['agov.eid.verification.link']}",
					"id": "${idvalue}",
					"error_code": "NONE"
				}}"""
			}
			else if (json.state == 'FAILED') {
				LOG
						.error("Eid verification failed: ${json.wallet_response.error_code} (${json.wallet_response.error_description})")
                       
                def status = ERROR_CODE_TO_STATUS_MAPPER[json.wallet_response.error_code] ?: 'ERROR'

                // Send new request & return variables with new id and url
                if(status == 'FAILED' || status == 'CANCELED'){
                    // Delete session variable to start a new verification
                    sess.removeAttribute('agov.eid.verification')

                    // Clear variables for for a cleaner result
                    sess.removeAttribute('agov.eid.verification.link')
                }

				result = """{
				"oid4vp": {
					"status": "${status}",
					"verification_url": "${session['agov.eid.verification.link']}",
					"id": "${idvalue}",
					"error_code": "${json.wallet_response.error_code}",
					"error_message": "${json.wallet_response.error_description}"
				}}"""
			}
			else {
				result = """{
				"oid4vp": {
					"status": "${inargs['o.id.v'] == 'NEW' || inargs['o.id.v'] == 'RESET' ? 'INITIATED' : 'PENDING'}",
					"verification_url": "${session['agov.eid.verification.link']}",
					"id": "${idvalue}",
					"error_code": "NONE"
				}}"""
			}
		}

	}
	catch (Exception e) {
		LOG.error("Eid verification failed: ${e}")
		result = """{
				"oid4vp": {
					"status": "ERROR",
					"verification_url": "${session['agov.eid.verification.link']}",
					"id": "${idvalue}",
					"error_code": "HTTP-ERROR",
					"error_message": "failed to verify status of verification ${idvalue}, http exception"
				}}"""
	}

	response.setContent(result.toString())
	response.setContentType('application/json')
	response.setHttpStatusCode(200)
	response.setIsDirectResponse(true)
	response.setStatus(AuthResponse.AUTH_CONTINUE)
	return
}

// if we reach this place, display GUI
LOG.debug("Show GUI")
response.setStatus(AuthResponse.AUTH_CONTINUE)
return

