import { AuthorizationResponseStateStatus } from '@sphereon/did-auth-siop'
import {
  AuthorizationServerMetadata,
  CredentialRequestV1_0_15,
  IssuerMetadata,
  Jwt,
  JWTHeader,
  JWTPayload,
  JwtVerifyResult,
  type OID4VCICredentialFormat,
  OpenId4VCIVersion,
  StatusListOpts,
} from '@sphereon/oid4vci-common'
import { CredentialDataSupplier, CredentialIssuanceInput, CredentialSignerCallback, VcIssuer, VcIssuerBuilder } from '@sphereon/oid4vci-issuer'
import { getAgentResolver, IDIDOptions } from '@sphereon/ssi-sdk-ext.did-utils'
import { legacyKeyRefsToIdentifierOpts, ManagedIdentifierOptsOrResult, ManagedIdentifierResult } from '@sphereon/ssi-sdk-ext.identifier-resolution'
import { contextHasPlugin } from '@sphereon/ssi-sdk.agent-config'
import { SdJwtVcPayload } from '@sphereon/ssi-sdk.sd-jwt'
import { IStatusListPlugin } from '@sphereon/ssi-sdk.vc-status-list'
import { CompactSdJwtVc, CredentialMapper, ICredential, W3CVerifiableCredential } from '@sphereon/ssi-types'
import { CredentialPayload, ProofFormat } from '@veramo/core'
import { bytesToBase64 } from '@veramo/utils'
import fetch from 'cross-fetch'
import { createJWT, decodeJWT, JWTVerifyOptions, verifyJWT } from 'did-jwt'
import { Resolvable } from 'did-resolver'
import { jwtDecode } from 'jwt-decode'
import { IIssuerOptions, IRequiredContext } from './types/IOID4VCIIssuer'

const CLOCK_SKEW_ISSUANCE = 2 // Clock drift on Android is typically up to 2000ms, so we issue 2 seconds in the past

export function getJwtVerifyCallback({ verifyOpts }: { verifyOpts?: JWTVerifyOptions }, _context: IRequiredContext) {
  return async (args: { jwt: string; kid?: string }): Promise<JwtVerifyResult> => {
    const resolver = getAgentResolver(_context, {
      resolverResolution: true,
      uniresolverResolution: true,
      localResolution: true,
    })
    verifyOpts = { ...verifyOpts, resolver: verifyOpts?.resolver } // Resolver separately as that is a function
    if (!verifyOpts?.resolver || typeof verifyOpts?.resolver?.resolve !== 'function') {
      verifyOpts.resolver = resolver
    }
    const result = await _context.agent.jwtVerifyJwsSignature({ jws: args.jwt })
    if (!result.error) {
      const identifier = result.jws.signatures[0].identifier
      if (!identifier) {
        return Promise.reject(Error('the jws did not contain a signature with an identifier'))
      }
      const jwkInfo = identifier.jwks[0]
      if (!jwkInfo) {
        return Promise.reject(Error(`the identifier of type ${identifier.method} is missing jwks (ExternalJwkInfo)`))
      }
      const { alg } = jwkInfo.jwk
      const header = jwtDecode<JWTHeader>(args.jwt, { header: true })
      const payload = jwtDecode<JWTPayload>(args.jwt, { header: false })
      const kid = args.kid ?? header.kid
      //const jwk = !kid ? jwkInfo.jwk : undefined // TODO double-check if this is correct
      const jwk = jwkInfo.jwk // FIXME workaround IATAB2B-57
      return {
        alg,
        ...identifier,
        jwt: { header, payload },
        ...(kid && { kid }),
        ...(jwk && { jwk }),
      } as JwtVerifyResult
    }

    const decodedJwt = (await decodeJWT(args.jwt)) as Jwt
    const kid = args.kid ?? decodedJwt.header.kid

    if (!kid || !kid.startsWith('did:')) {
      // No DID method present in header. We already performed the validation above. So return that
      return {
        alg: decodedJwt.header.alg,
        jwt: decodedJwt,
      } as JwtVerifyResult
    }
    const did = kid.split('#')[0]

    const didResult = await verifyJWT(args.jwt, verifyOpts)
    if (!didResult.verified) {
      console.log(`JWT invalid: ${args.jwt}`)
      throw Error('JWT did not verify successfully')
    }

    const didResolution = await resolver.resolve(did)
    if (!didResolution || !didResolution.didDocument) {
      throw Error(`Could not resolve did: ${did}, metadata: ${didResolution?.didResolutionMetadata}`)
    }

    const alg = decodedJwt.header.alg
    return {
      alg,
      kid,
      did,
      didDocument: didResolution.didDocument,
      jwt: decodedJwt,
    }
  }
}

export async function getAccessTokenKeyRef(
  opts: {
    /**
     * Uniform identifier options
     */
    idOpts?: ManagedIdentifierOptsOrResult
    /**
     * @deprecated
     */
    iss?: string
    /**
     * @deprecated
     */
    keyRef?: string
    /**
     * @deprecated
     */
    didOpts?: IDIDOptions
  },
  context: IRequiredContext,
) {
  let identifier = legacyKeyRefsToIdentifierOpts(opts)
  return await context.agent.identifierManagedGet(identifier)
}

export async function getAccessTokenSignerCallback(
  opts: {
    /**
     * Uniform identifier options
     */
    idOpts?: ManagedIdentifierOptsOrResult
    /**
     * @deprecated
     */
    iss?: string
    /**
     * @deprecated
     */
    keyRef?: string
    /**
     * @deprecated
     */
    didOpts?: IDIDOptions
  },
  context: IRequiredContext,
) {
  const resolution = legacyKeyRefsToIdentifierOpts(opts)
  const identifier = await context.agent.identifierManagedGet({
    identifier: resolution.identifier as string,
    vmRelationship: 'authentication',
  })

  const keyRef = identifier.kmsKeyRef
  if (!keyRef) {
    throw Error('Cannot sign access tokens without a key ref')
  }

  const signer = async (data: string | Uint8Array) => {
    let dataString, encoding: 'base64' | undefined

    if (typeof data === 'string') {
      dataString = data
      encoding = undefined
    } else {
      dataString = bytesToBase64(data)
      encoding = 'base64'
    }
    return context.agent.keyManagerSign({ keyRef, data: dataString, encoding })
  }

  async function accessTokenSignerCallback(jwt: Jwt, kid?: string): Promise<string> {
    const issuer =
      opts.idOpts?.issuer ??
      (typeof opts.idOpts?.identifier === 'string' ? opts.idOpts.identifier : (opts.didOpts?.idOpts?.identifier?.toString() ?? opts?.iss))
    if (!issuer) {
      throw Error('No issuer configured for access tokens')
    }

    let kidHeader: string | undefined = jwt?.header?.kid ?? kid
    if (!kidHeader && identifier.kid) {
      kidHeader = identifier.kid
    }
    if (!kidHeader) {
      if (
        opts.idOpts?.method === 'did' ||
        opts.idOpts?.method === 'kid' ||
        (typeof opts.didOpts?.idOpts.identifier === 'string' && opts.didOpts?.idOpts?.identifier?.startsWith('did:'))
      ) {
        // @ts-ignore
        kidHeader = opts.idOpts?.kid ?? opts.didOpts?.idOpts?.kid ?? opts?.didOpts?.identifierOpts?.kid
      }
    }

    const alg = identifier.jwk?.alg
    if (!alg) {
      return Promise.reject(Error('No algorithm found in identifier JWK'))
    }

    return await createJWT(jwt.payload, { signer, issuer }, { ...jwt.header, ...(kidHeader && { kid: kidHeader }), typ: 'JWT', alg })
  }

  return accessTokenSignerCallback
}

export async function getCredentialSignerCallback(
  idOpts: ManagedIdentifierOptsOrResult & {
    crypto?: Crypto
  },
  context: IRequiredContext,
): Promise<CredentialSignerCallback> {
  async function issueVCCallback(args: {
    credentialRequest: CredentialRequestV1_0_15
    credential: CredentialIssuanceInput
    jwtVerifyResult: JwtVerifyResult
    format?: OID4VCICredentialFormat
    statusLists?: Array<StatusListOpts>
  }): Promise<W3CVerifiableCredential | CompactSdJwtVc> {
    const { jwtVerifyResult, format, statusLists } = args
    const credential = args.credential as ICredential // TODO: SDJWT
    let proofFormat: ProofFormat

    let resolution: ManagedIdentifierResult
    if (typeof idOpts.identifier !== 'string') {
      resolution = idOpts as ManagedIdentifierResult
    } else {
      resolution = await context.agent.identifierManagedGet({
        identifier: idOpts.identifier,
        vmRelationship: 'assertionMethod',
      })
    }
    proofFormat = format?.includes('ld') ? 'lds' : 'jwt'
    const issuer = resolution.issuer ?? resolution.kmsKeyRef

    if (CredentialMapper.isW3cCredential(credential)) {
      if (!credential.issuer) {
        credential.issuer = { id: issuer }
      } else if (typeof credential.issuer === 'object' && !credential.issuer.id) {
        credential.issuer.id = issuer
      }
      const subjectIsArray = Array.isArray(credential.credentialSubject)
      let credentialSubjects = Array.isArray(credential.credentialSubject) ? credential.credentialSubject : [credential.credentialSubject]
      credentialSubjects = credentialSubjects.map((subject) => {
        if (!subject.id) {
          subject.id = jwtVerifyResult.did
        }
        return subject
      })
      credential.credentialSubject = subjectIsArray ? credentialSubjects : credentialSubjects[0]

      // TODO: We should extend the plugin capabilities of issuance so we do not have to tuck this into the sign callback
      if (contextHasPlugin<IStatusListPlugin>(context, 'slAddStatusToCredential')) {
        // Add status list if enabled (and when the input has a credentialStatus object (can be empty))
        const credentialStatusVC = await context.agent.slAddStatusToCredential({ credential, statusLists })
        if (credential.credentialStatus && !credential.credentialStatus.statusListCredential) {
          credential.credentialStatus = credentialStatusVC.credentialStatus
        }
      }

      const result = await context.agent.createVerifiableCredential({
        credential: credential as CredentialPayload,
        proofFormat,
        removeOriginalFields: false,
        fetchRemoteContexts: true,
        domain: typeof credential.issuer === 'object' ? credential.issuer.id : credential.issuer,
        ...(resolution.kid && { header: { kid: resolution.kid } }),
      })
      return (proofFormat === 'jwt' && 'jwt' in result.proof ? result.proof.jwt : result) as W3CVerifiableCredential
    } else if (CredentialMapper.isSdJwtDecodedCredentialPayload(credential)) {
      const sdJwtPayload = credential as SdJwtVcPayload
      if (sdJwtPayload.iss === undefined) {
        sdJwtPayload.iss = issuer
      }
      if (sdJwtPayload.iat === undefined) {
        sdJwtPayload.iat = Math.floor(new Date().getTime() / 1000) - CLOCK_SKEW_ISSUANCE
      }

      let disclosureFrame
      if ('disclosureFrame' in credential) {
        disclosureFrame = credential['disclosureFrame']
        delete credential['disclosureFrame']
      } else {
        disclosureFrame = {
          _sd: credential['_sd'],
        }
      }

      if (contextHasPlugin<IStatusListPlugin>(context, 'slAddStatusToSdJwtCredential')) {
        if ((sdJwtPayload.status && sdJwtPayload.status.status_list) || (statusLists && statusLists.length > 0)) {
          // Add status list if enabled (and when the input has a credentialStatus object (can be empty))
          const sdJwtPayloadWithStatus = await context.agent.slAddStatusToSdJwtCredential({ credential: sdJwtPayload, statusLists })
          if (sdJwtPayload.status?.status_list?.idx) {
            if (!sdJwtPayloadWithStatus.status || !sdJwtPayloadWithStatus.status.status_list) {
              // sdJwtPayload and sdJwtPayloadWithStatus is the same for now, but we should use the result anyway as this could be subject to change
              return Promise.reject(Error('slAddStatusToSdJwtCredential did not return a status_list'))
            }

            // Update statusListId & statusListIndex back to the credential session TODO SSISDK-4 This is not a clean way to do this.
            if (statusLists && statusLists.length > 0) {
              const statusList = statusLists[0]
              statusList.statusListId = sdJwtPayloadWithStatus.status.status_list.uri
              statusList.statusListIndex = sdJwtPayloadWithStatus.status.status_list.idx
            }
            sdJwtPayload.status.status_list.idx = sdJwtPayloadWithStatus.status.status_list.idx
          }
        }
      }

      const result = await context.agent.createSdJwtVc({
        credentialPayload: sdJwtPayload,
        disclosureFrame: disclosureFrame,
        resolution,
      })
      return result.credential
    } /*else if (CredentialMapper.isMsoMdocDecodedCredential(credential)) {
      TODO
    }*/
    return Promise.reject('VC issuance failed, an incorrect or unsupported credential was supplied')
  }

  return issueVCCallback
}

export async function createVciIssuerBuilder(
  args: {
    issuerOpts: IIssuerOptions
    issuerMetadata: IssuerMetadata
    authorizationServerMetadata: AuthorizationServerMetadata
    resolver?: Resolvable
    credentialDataSupplier?: CredentialDataSupplier
  },
  context: IRequiredContext,
): Promise<VcIssuerBuilder> {
  const { issuerOpts, issuerMetadata, authorizationServerMetadata } = args

  const builder = new VcIssuerBuilder()
  // @ts-ignore
  const resolver =
    args.resolver ??
    args?.issuerOpts?.didOpts?.resolveOpts?.resolver ??
    args.issuerOpts?.didOpts?.resolveOpts?.jwtVerifyOpts?.resolver ??
    getAgentResolver(context)
  if (!resolver) {
    throw Error('A Resolver is necessary to verify DID JWTs')
  }
  const idOpts = legacyKeyRefsToIdentifierOpts({ didOpts: issuerOpts.didOpts, idOpts: issuerOpts.idOpts })
  const jwtVerifyOpts: JWTVerifyOptions = {
    ...issuerOpts?.didOpts?.resolveOpts?.jwtVerifyOpts,
    ...args?.issuerOpts?.resolveOpts?.jwtVerifyOpts,
    resolver,
    audience: issuerMetadata.credential_issuer as string, // FIXME legacy version had {display: NameAndLocale | NameAndLocale[]} as credential_issuer
  }
  builder.withVersion(issuerOpts.version ?? OpenId4VCIVersion.VER_1_0)
  builder.withIssuerMetadata(issuerMetadata)
  builder.withAuthorizationMetadata(authorizationServerMetadata)
  // builder.withUserPinRequired(issuerOpts.userPinRequired ?? false) was removed from implementers draft v1
  builder.withCredentialSignerCallback(await getCredentialSignerCallback(idOpts, context))
  if (issuerOpts.nonceEndpoint) {
    builder.withNonceEndpoint(issuerOpts.nonceEndpoint)
  } else if (issuerMetadata.nonce_endpoint) {
    builder.withNonceEndpoint(issuerOpts.nonceEndpoint ?? issuerMetadata.nonce_endpoint)
  }

  if (issuerOpts.asClientOpts) {
    builder.withASClientMetadata(issuerOpts.asClientOpts)
    // @ts-ignore
    // const authorizationServer = issuerMetadata.authorization_servers[0] as string
    // Set the OIDC verifier
    // builder.withJWTVerifyCallback(oidcAccessTokenVerifyCallback({clientMetadata: issuerOpts.asClientOpts, credentialIssuer: issuerMetadata.credential_issuer as string, authorizationServer}))
  }
  // Do not use it when asClient is used
  builder.withJWTVerifyCallback(getJwtVerifyCallback({ verifyOpts: jwtVerifyOpts }, context))

  if (args.credentialDataSupplier) {
    builder.withCredentialDataSupplier(args.credentialDataSupplier)
  }
  builder.withInMemoryCNonceState()
  builder.withInMemoryCredentialOfferState()
  builder.withInMemoryCredentialOfferURIState()

  return builder
}

export async function createVciIssuer(
  {
    issuerOpts,
    issuerMetadata,
    authorizationServerMetadata,
    credentialDataSupplier,
  }: {
    issuerOpts: IIssuerOptions
    issuerMetadata: IssuerMetadata
    authorizationServerMetadata: AuthorizationServerMetadata
    credentialDataSupplier?: CredentialDataSupplier
  },
  context: IRequiredContext,
): Promise<VcIssuer> {
  return (
    await createVciIssuerBuilder(
      {
        issuerOpts,
        issuerMetadata,
        authorizationServerMetadata,
        credentialDataSupplier,
      },
      context,
    )
  ).build()
}

export async function createAuthRequestUriCallback(opts: { path: string; presentationDefinitionId: string }): Promise<() => Promise<string>> {
  async function authRequestUriCallback(): Promise<string> {
    const path = opts.path.replace(':definitionId', opts.presentationDefinitionId)
    return fetch(path, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
    }).then(async (response): Promise<string> => {
      if (response.status >= 400) {
        return Promise.reject(Error(await response.text()))
      } else {
        const responseData = await response.json()

        if (!responseData.authRequestURI) {
          return Promise.reject(Error('Missing auth request uri in response body'))
        }

        return responseData.authRequestURI
      }
    })
  }

  return authRequestUriCallback
}

export async function createVerifyAuthResponseCallback(opts: {
  path: string
  presentationDefinitionId: string
}): Promise<(correlationId: string) => Promise<boolean>> {
  async function verifyAuthResponseCallback(correlationId: string): Promise<boolean> {
    return fetch(opts.path, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ definitionId: opts.presentationDefinitionId, correlationId }),
    }).then(async (response): Promise<boolean> => {
      if (response.status >= 400) {
        return Promise.reject(Error(await response.text()))
      } else {
        const responseData = await response.json()

        if (!responseData.status) {
          return Promise.reject(Error('Missing status in response body'))
        }

        return responseData.status === AuthorizationResponseStateStatus.VERIFIED
      }
    })
  }

  return verifyAuthResponseCallback
}
