import { OriginalType, WrappedVerifiableCredential, WrappedVerifiablePresentation } from './vc'
import { decodeSdJwt, decodeSdJwtSync, getClaims, getClaimsSync } from '@sd-jwt/decode'
import { CompactJWT, IVerifiableCredential } from './w3c-vc'
import { IProofPurpose, IProofType } from './did'
import { OrPromise } from './generic'

type JsonValue = string | number | boolean | { [x: string]: JsonValue | undefined } | Array<JsonValue>

type SdJwtJsonValue =
  | string
  | number
  | boolean
  | {
      [x: string]: SdJwtJsonValue | undefined
      _sd?: string[]
    }
  | Array<SdJwtJsonValue | { '...': string }>

/**
 * Decoded 'pretty' SD JWT Verifiable Credential. This representation has all the `_sd` properties
 * removed, and includes the disclosures directly within the payload.
 */
export interface SdJwtDecodedVerifiableCredentialPayload {
  vct: string
  iss: string
  iat: number
  nbf?: number
  exp?: number
  cnf?: {
    jwk?: any
    kid?: string
  }
  status?: {
    idx: number
    uri: string
  }
  sub?: string

  [key: string]: JsonValue | undefined
}

/**
 * Represents a selective disclosure JWT vc in compact form.
 */
export type CompactSdJwtVc = string

/**
 * The signed payload of an SD-JWT. Includes fields such as `_sd`, `...` and `_sd_alg`
 */
interface SdJwtSignedVerifiableCredentialPayload extends SdJwtDecodedVerifiableCredentialPayload {
  // Only present if there are any selectively discloseable claims
  _sd?: string[]
  _sd_alg?: string

  [x: string]: SdJwtJsonValue | undefined
}

type SdJwtFrameValue = boolean | Array<SdJwtFrameValue> | { [x: string]: SdJwtFrameValue }
export type SdJwtDisclosureFrame = Record<string, SdJwtFrameValue>
export type SdJwtPresentationFrame = Record<string, SdJwtFrameValue>

/**
 * Input for creating a SD JWT Verifiable Credential. This representation optionally includes the disclosure frame,
 * (as `__disclosureFrame`) to indicate which fields in the signed SD-JWT should be selectively discloseable
 */
export interface SdJwtCredentialInput extends SdJwtDecodedVerifiableCredentialPayload {
  /**
   * Disclosure frame, indicating which fields in the signed SD-JWT should be selectively discloseable
   * Will be removed from the actual SD-JWT payload before signing
   */
  __disclosureFrame?: SdJwtDisclosureFrame
}

export type SdJwtDecodedDisclosure = [string, string, JsonValue] | [string, JsonValue]
export interface SdJwtDisclosure {
  // The encoded disclosure
  encoded: string

  // The decoded disclosure, in format [salt, claim, value] or in case of array entry [salt, value]
  decoded: SdJwtDecodedDisclosure

  // Digest over disclosure, can be used to match against a value within the SD JWT payload
  digest: string
}

/**
 * The decoded SD JWT Verifiable Credential. This representation includes multiple representations of the
 * same SD-JWT, and allows to fully process an SD-JWT, as well as create a presentation SD-JWT  (minus the KB-JWT) by removing
 * certain disclosures from the compact SD-JWT.
 *
 * This representation is useful as it doesn't require a hasher implementation to match the different digests in the signed SD-JWT
 * payload, with the different disclosures.
 */
export interface SdJwtDecodedVerifiableCredential {
  /**
   * The compact sd jwt is the sd-jwt encoded as string. It is a normal JWT,
   * with the disclosures and kb-jwt appended separated by ~ */
  compactSdJwtVc: string

  /**
   * The disclosures included within the SD-JWT in both encoded and decoded format.
   * The digests are also included, and allows the disclosures to be linked against
   * the digests in the signed payload.
   */
  disclosures: Array<SdJwtDisclosure>

  /**
   * The signed payload is the payload of the sd-jwt that is actually signed, and that includes
   * the `_sd` and `...` digests.
   */
  signedPayload: SdJwtSignedVerifiableCredentialPayload

  /**
   * The decoded payload is the payload when all `_sd` and `...` digests have been replaced
   * by the actual values from the disclosures. This format could also be seen as the 'pretty`
   * version of the SD JWT payload.
   *
   * This is useful for displaying the contents of the SD JWT VC to the user, or for example
   * for querying the contents of the SD JWT VC using a PEX presentation definition path.
   */
  decodedPayload: SdJwtDecodedVerifiableCredentialPayload

  /**
   * Key binding JWT
   */
  kbJwt?: {
    header: SdJwtVcKbJwtHeader
    payload: SdJwtVcKbJwtPayload
    compact?: CompactJWT
  }
}

export interface SdJwtVcKbJwtHeader {
  typ: 'kb+jwt'
  alg: string
  [x: string]: any
}

export interface SdJwtVcKbJwtPayload {
  iat: number
  aud: string
  nonce: string
  sd_hash: string
  [key: string]: unknown
}

export interface WrappedSdJwtVerifiableCredential {
  /**
   * Original VC that we've received. Can be either the encoded or decoded variant.
   */
  original: SdJwtDecodedVerifiableCredential | CompactSdJwtVc
  /**
   * Decoded version of the SD-JWT payload. This is the decoded payload, rather than the whole SD-JWT as the `decoded` property
   * is used in e.g. PEX to check for path filters from fields. The full decoded credential can be found in the `credential` field.
   */
  decoded: SdJwtDecodedVerifiableCredentialPayload
  /**
   * Type of this credential.
   */
  type: OriginalType.SD_JWT_VC_DECODED | OriginalType.SD_JWT_VC_ENCODED
  /**
   * The claim format, typically used during exchange transport protocols
   */
  format: 'vc+sd-jwt'
  /**
   * Internal stable representation of a Credential
   */
  credential: SdJwtDecodedVerifiableCredential
}

export type HasherSync = (data: string | ArrayBuffer, alg: string) => Uint8Array
export type Hasher = (data: string | ArrayBuffer, alg: string) => OrPromise<Uint8Array>

export interface WrappedSdJwtVerifiablePresentation {
  /**
   * Original VP that we've received. Can be either the encoded or decoded variant.
   */
  original: SdJwtDecodedVerifiableCredential | CompactSdJwtVc
  /**
   * Decoded version of the SD-JWT payload. This is the decoded payload, rather than the whole SD-JWT.
   */
  decoded: SdJwtDecodedVerifiableCredentialPayload
  /**
   * Type of this Presentation.
   */
  type: OriginalType.SD_JWT_VC_DECODED | OriginalType.SD_JWT_VC_ENCODED
  /**
   * The claim format, typically used during exchange transport protocols
   */
  format: 'vc+sd-jwt'
  /**
   * Internal stable representation of a Presentation
   */
  presentation: SdJwtDecodedVerifiableCredential
  /**
   * Wrapped Verifiable Credentials belonging to the Presentation. Will always be an array
   * with a single SdJwtVerifiableCredential entry.
   */
  vcs: [WrappedSdJwtVerifiableCredential]
}

export function isWrappedSdJwtVerifiableCredential(vc: WrappedVerifiableCredential): vc is WrappedSdJwtVerifiableCredential {
  return vc.format === 'vc+sd-jwt'
}

export function isWrappedSdJwtVerifiablePresentation(vp: WrappedVerifiablePresentation): vp is WrappedSdJwtVerifiablePresentation {
  return vp.format === 'vc+sd-jwt'
}

/**
 * Decode an SD-JWT vc from its compact format (string) to an object containing the disclosures,
 * signed payload, decoded payload and the compact SD-JWT vc.
 *
 * Both the input and output interfaces of this method are defined in `@sphereon/ssi-types`, so
 * this method hides the actual implementation of SD-JWT (which is currently based on @sd-jwt/core)
 */
export function decodeSdJwtVc(compactSdJwtVc: CompactSdJwtVc, hasher: HasherSync): SdJwtDecodedVerifiableCredential {
  const { jwt, disclosures, kbJwt } = decodeSdJwtSync(compactSdJwtVc, hasher)

  const signedPayload = jwt.payload as SdJwtSignedVerifiableCredentialPayload
  const decodedPayload = getClaimsSync(signedPayload, disclosures, hasher)
  const compactKeyBindingJwt = kbJwt ? compactSdJwtVc.split('~').pop() : undefined

  return {
    compactSdJwtVc,
    decodedPayload: decodedPayload as SdJwtDecodedVerifiableCredentialPayload,
    disclosures: disclosures.map((d) => {
      const decoded = d.key ? [d.salt, d.key, d.value] : [d.salt, d.value]
      if (!d._digest) throw new Error('Implementation error: digest not present in disclosure')
      return {
        decoded: decoded as SdJwtDecodedDisclosure,
        digest: d._digest,
        encoded: d.encode(),
      } satisfies SdJwtDisclosure
    }),
    signedPayload: signedPayload as SdJwtSignedVerifiableCredentialPayload,
    ...(compactKeyBindingJwt &&
      kbJwt && {
        kbJwt: {
          header: kbJwt.header as SdJwtVcKbJwtHeader,
          compact: compactKeyBindingJwt,
          payload: kbJwt.payload as SdJwtVcKbJwtPayload,
        },
      }),
  }
}

/**
 * Decode an SD-JWT vc from its compact format (string) to an object containing the disclosures,
 * signed payload, decoded payload and the compact SD-JWT vc.
 *
 * Both the input and output interfaces of this method are defined in `@sphereon/ssi-types`, so
 * this method hides the actual implementation of SD-JWT (which is currently based on @sd-jwt/core)
 */
export async function decodeSdJwtVcAsync(compactSdJwtVc: CompactSdJwtVc, hasher: Hasher): Promise<SdJwtDecodedVerifiableCredential> {
  const { jwt, disclosures, kbJwt } = await decodeSdJwt(compactSdJwtVc, hasher)

  const signedPayload = jwt.payload as SdJwtSignedVerifiableCredentialPayload
  const decodedPayload = await getClaims(signedPayload, disclosures, hasher)
  const compactKeyBindingJwt = kbJwt ? compactSdJwtVc.split('~').pop() : undefined

  return {
    compactSdJwtVc,
    decodedPayload: decodedPayload as SdJwtDecodedVerifiableCredentialPayload,
    disclosures: disclosures.map((d) => {
      const decoded = d.key ? [d.salt, d.key, d.value] : [d.salt, d.value]
      if (!d._digest) throw new Error('Implementation error: digest not present in disclosure')
      return {
        decoded: decoded as SdJwtDecodedDisclosure,
        digest: d._digest,
        encoded: d.encode(),
      } satisfies SdJwtDisclosure
    }),
    signedPayload: signedPayload as SdJwtSignedVerifiableCredentialPayload,
    ...(compactKeyBindingJwt &&
      kbJwt && {
        kbJwt: {
          header: kbJwt.header as SdJwtVcKbJwtHeader,
          payload: kbJwt.payload as SdJwtVcKbJwtPayload,
          compact: compactKeyBindingJwt,
        },
      }),
  }
}

export const sdJwtDecodedCredentialToUniformCredential = (
  decoded: SdJwtDecodedVerifiableCredential,
  opts?: { maxTimeSkewInMS?: number },
): IVerifiableCredential => {
  const { decodedPayload } = decoded
  const { exp, nbf, iss, iat, vct, cnf, status, sub, jti } = decodedPayload

  const maxSkewInMS = opts?.maxTimeSkewInMS ?? 1500

  const expirationDate = jwtDateToISOString({ jwtClaim: exp, claimName: 'exp' })
  let issuanceDateStr = jwtDateToISOString({ jwtClaim: iat, claimName: 'iat' })

  let nbfDateAsStr: string | undefined
  if (nbf) {
    nbfDateAsStr = jwtDateToISOString({ jwtClaim: nbf, claimName: 'nbf' })
    if (issuanceDateStr && nbfDateAsStr && issuanceDateStr !== nbfDateAsStr) {
      const diff = Math.abs(new Date(nbfDateAsStr).getTime() - new Date(iss).getTime())
      if (!maxSkewInMS || diff > maxSkewInMS) {
        throw Error(`Inconsistent issuance dates between JWT claim (${nbfDateAsStr}) and VC value (${iss})`)
      }
    }
    issuanceDateStr = nbfDateAsStr
  }
  const issuanceDate = issuanceDateStr
  if (!issuanceDate) {
    throw Error(`JWT issuance date is required but was not present`)
  }

  // Filter out the fields we don't want in credentialSubject
  const excludedFields = new Set(['vct', 'cnf', 'iss', 'iat', 'exp', 'nbf', 'jti', 'sub'])
  const credentialSubject = Object.entries(decodedPayload).reduce(
    (acc, [key, value]) => {
      if (
        !excludedFields.has(key) &&
        value !== undefined &&
        value !== '' &&
        !(typeof value === 'object' && value !== null && Object.keys(value).length === 0)
      ) {
        acc[key] = value
      }
      return acc
    },
    {} as Record<string, any>,
  )

  const credential: Omit<IVerifiableCredential, 'issuer' | 'issuanceDate'> = {
    type: [vct], // SDJwt is not a W3C VC, so no VerifiableCredential
    '@context': [], // SDJwt has no JSON-LD by default. Certainly not the VC DM1 default context for JSON-LD
    credentialSubject: {
      ...credentialSubject,
      id: credentialSubject.id ?? sub ?? jti,
    },
    issuanceDate,
    expirationDate,
    issuer: iss,
    ...(cnf && { cnf }),
    ...(status && { status }),
    proof: {
      type: IProofType.SdJwtProof2024,
      created: nbfDateAsStr ?? issuanceDate,
      proofPurpose: IProofPurpose.authentication,
      verificationMethod: iss,
      jwt: decoded.compactSdJwtVc,
    },
  }

  return credential as IVerifiableCredential
}

const jwtDateToISOString = ({
  jwtClaim,
  claimName,
  isRequired = false,
}: {
  jwtClaim?: number
  claimName: string
  isRequired?: boolean
}): string | undefined => {
  if (jwtClaim) {
    const claim = parseInt(jwtClaim.toString())
    // change JWT seconds to millisecond for the date
    return new Date(claim * (claim < 9999999999 ? 1000 : 1)).toISOString().replace(/\.000Z/, 'Z')
  } else if (isRequired) {
    throw Error(`JWT claim ${claimName} is required but was not present`)
  }
  return undefined
}
