import { IPresentationDefinition } from '@sphereon/pex'
import { Format } from '@sphereon/pex-models'
import {
  isManagedIdentifierDidOpts,
  isManagedIdentifierDidResult,
  isManagedIdentifierX5cResult,
  ManagedIdentifierOptsOrResult,
} from '@sphereon/ssi-sdk-ext.identifier-resolution'
import {
  CredentialMapper,
  Optional,
  OriginalVerifiablePresentation,
  SdJwtDecodedVerifiableCredential,
  W3CVerifiablePresentation,
} from '@sphereon/ssi-types'
import { PresentationPayload, ProofFormat } from '@veramo/core'
import { IPEXPresentationSignCallback, IRequiredContext } from './types/IPresentationExchange'

export async function createPEXPresentationSignCallback(
  args: {
    idOpts: ManagedIdentifierOptsOrResult
    fetchRemoteContexts?: boolean
    skipDidResolution?: boolean
    format?: Format | ProofFormat
    domain?: string
    challenge?: string
  },
  context: IRequiredContext,
): Promise<IPEXPresentationSignCallback> {
  function determineProofFormat(determineArgs: {
    format?: Format | 'jwt' | 'lds' | 'EthereumEip712Signature2021'
    presentationDefinition: IPresentationDefinition
    presentation: Optional<PresentationPayload, 'holder'> | SdJwtDecodedVerifiableCredential
  }): string {
    const { format, presentationDefinition, presentation } = determineArgs

    var formatOptions = format ?? presentationDefinition.format ?? args.format
    // TODO Refactor so it takes into account the Input Descriptors and we can lookup from there. Now we only do that if there is 1 descriptor
    if (!formatOptions && presentationDefinition.input_descriptors.length == 1 && 'format' in presentationDefinition.input_descriptors[0]) {
      formatOptions = presentationDefinition.input_descriptors[0].format
    }
    // All format arguments are optional. So if no format has been given we go for the most supported 'jwt'
    if (!formatOptions) {
      if (CredentialMapper.isSdJwtDecodedCredentialPayload(presentation.decodedPayload)) {
        return 'vc+sd-jwt'
      } else if (CredentialMapper.isMsoMdocDecodedPresentation(presentation.decodedPayload as OriginalVerifiablePresentation)) {
        return 'mso_mdoc'
      } else if (CredentialMapper.isW3cPresentation(presentation.decodedPayload)) {
        if (typeof presentation.signedPayload === 'string') {
          return 'jwt'
        }
        return 'lds'
      }
      return 'jwt'
    } else if (typeof formatOptions === 'string') {
      // if formatOptions is a singular string we can return that as the format
      return formatOptions
    }

    // here we transform all format options to either lds or jwt. but we also want to support sd-jwt, so we need to specifically check for this one. which is ['vc+sd-jwt']
    const formats = new Set(
      Object.keys(formatOptions).map((form) => (form.includes('ldp') ? 'lds' : form.includes('vc+sd-jwt') ? 'vc+sd-jwt' : 'jwt')),
    )

    // if we only have 1 format type we can return that
    if (formats.size === 1) {
      return formats.values().next().value!!
    }
    formats.keys().next()
    // if we can go for sd-jwt, we go for sd-jwt
    if (formats.has('vc+sd-jwt')) {
      return 'vc+sd-jwt'
    }
    // if it is not sd-jwt we would like to go for jwt
    else if (formats.has('jwt')) {
      return 'jwt'
    }

    // else we go for lds
    return 'lds'
  }

  return async ({
    presentation,
    domain,
    presentationDefinition,
    format,
    challenge,
  }: {
    presentation: Optional<PresentationPayload, 'holder'> | SdJwtDecodedVerifiableCredential
    presentationDefinition: IPresentationDefinition
    format?: Format | ProofFormat
    domain?: string
    challenge?: string
  }): Promise<W3CVerifiablePresentation> => {
    const proofFormat = determineProofFormat({ format, presentationDefinition, presentation })
    const { idOpts } = args
    const CLOCK_SKEW = 120
    if (args.skipDidResolution && isManagedIdentifierDidOpts(idOpts)) {
      idOpts.offlineWhenNoDIDRegistered = true
    }

    if ('compactSdJwtVc' in presentation) {
      if (proofFormat !== 'vc+sd-jwt') {
        return Promise.reject(Error(`presentation payload does not match proof format ${proofFormat}`))
      }

      const presentationResult = await context.agent.createSdJwtPresentation({
        ...(idOpts?.method === 'oid4vci-issuer' && { holder: idOpts?.issuer as string }),
        presentation: presentation.compactSdJwtVc,
        kb: {
          payload: {
            ...presentation.kbJwt?.payload,
            iat: presentation.kbJwt?.payload?.iat ?? Math.floor(Date.now() / 1000 - CLOCK_SKEW),
            nonce: challenge ?? presentation.kbJwt?.payload?.nonce,
            aud: presentation.kbJwt?.payload?.aud ?? domain ?? args.domain,
          },
        },
      })

      return CredentialMapper.storedPresentationToOriginalFormat(presentationResult.presentation as OriginalVerifiablePresentation)
    } else {
      const resolution = await context.agent.identifierManagedGet(idOpts)

      if (proofFormat === 'vc+sd-jwt') {
        return Promise.reject(Error(`presentation payload does not match proof format ${proofFormat}`))
      }
      let header
      if (!presentation.holder) {
        presentation.holder = resolution.issuer
      }
      if (proofFormat === 'jwt') {
        header = {
          ...((isManagedIdentifierDidResult(resolution) || isManagedIdentifierX5cResult(resolution)) && resolution.kid && { kid: resolution.kid }),
          ...(isManagedIdentifierX5cResult(resolution) && { jwk: resolution.jwk }),
        }
        if (presentation.verifier || !presentation.aud) {
          presentation.aud = Array.isArray(presentation.verifier) ? presentation.verifier : (presentation.verifier ?? domain ?? args.domain)
          delete presentation.verifier
        }

        if (!presentation.nbf) {
          if (presentation.issuanceDate) {
            const converted = Date.parse(presentation.issuanceDate)
            if (!isNaN(converted)) {
              presentation.nbf = Math.floor(converted / 1000) // no skew here, as an explicit value was given
            }
          } else {
            presentation.nbf = Math.floor(Date.now() / 1000 - CLOCK_SKEW)
          }
        }

        if (!presentation.iat) {
          presentation.iat = presentation.nbf
        }

        if (!presentation.exp) {
          if (presentation.expirationDate) {
            const converted = Date.parse(presentation.expirationDate)
            if (!isNaN(converted)) {
              presentation.exp = Math.floor(converted / 1000) // no skew here as an explicit value w as given
            }
          } else {
            presentation.exp = presentation.nbf + 600 + CLOCK_SKEW
          }
        }

        if (!presentation.vp) {
          presentation.vp = {}
        }
        /*if (!presentation.sub) {
          presentation.sub = id.did
        }*/
        if (!presentation.vp.holder) {
          presentation.vp.holder = presentation.holder
        }
      }

      // we ignore the alg / proof_format for now, as we already have the kid anyway at this point

      // todo: look for jwt_vc_json and remove types and @context

      const vp = await context.agent.createVerifiablePresentation({
        presentation: presentation as PresentationPayload,
        removeOriginalFields: false,
        keyRef: resolution.kmsKeyRef,
        // domain: domain ?? args.domain, // handled above, and did-jwt-vc creates an array even for 1 entry
        challenge: challenge ?? args.challenge,
        fetchRemoteContexts: args.fetchRemoteContexts !== false,
        proofFormat: proofFormat as ProofFormat,
        header,
      })

      // makes sure we extract an actual JWT from the internal representation in case it is a JWT
      return CredentialMapper.storedPresentationToOriginalFormat(vp as OriginalVerifiablePresentation)
    }
  }
}
