'use strict';

var jsonld = require('jsonld');
var jsigs = require('jsonld-signatures');
var cryptoWasmTs = require('@docknetwork/crypto-wasm-ts');
var b58 = require('bs58');
var vcDelegationEngine = require('@docknetwork/vc-delegation-engine');
var vc_credentials = require('./credentials.cjs');
var vc_crypto_common_DockCryptoSignature = require('./crypto/common/DockCryptoSignature.cjs');
var vc_documentLoader = require('./document-loader.cjs');
var vc_helpers = require('./helpers.cjs');
var vc_crypto_constants = require('./crypto/constants.cjs');
var vc_constants = require('./constants.cjs');
require('./crypto/Bls12381BBDT16KeyPairDock2024.cjs');
require('./crypto/Bls12381BBDT16MACDock2024.cjs');
require('./crypto/Bls12381BBDT16MACProofDock2024.cjs');
require('./crypto/Bls12381BBSKeyPairDock2023.cjs');
var vc_crypto_Bls12381BBSSignatureDock2022 = require('./crypto/Bls12381BBSSignatureDock2022.cjs');
var vc_crypto_Bls12381BBSSignatureDock2023 = require('./crypto/Bls12381BBSSignatureDock2023.cjs');
require('./crypto/Bls12381BBSSignatureProofDock2022.cjs');
require('./crypto/Bls12381BBSSignatureProofDock2023.cjs');
require('./crypto/Bls12381BDDT16KeyPairDock2024.cjs');
require('./crypto/Bls12381BDDT16MACDock2024.cjs');
require('./crypto/Bls12381BDDT16MACProofDock2024.cjs');
require('./crypto/Bls12381G2KeyPairDock2022.cjs');
require('./crypto/Bls12381PSKeyPairDock2023.cjs');
var vc_crypto_Bls12381PSSignatureDock2023 = require('./crypto/Bls12381PSSignatureDock2023.cjs');
require('./crypto/Bls12381PSSignatureProofDock2023.cjs');
var vc_crypto_EcdsaSecp256k1Signature2019 = require('./crypto/EcdsaSecp256k1Signature2019.cjs');
require('@juanelas/base64');
require('crypto');
require('../keypairs/keypair-secp256k1.cjs');
require('multiformats/bases/base58');
var vc_crypto_Ed25519Signature2018 = require('./crypto/Ed25519Signature2018.cjs');
var vc_crypto_Ed25519Signature2020 = require('./crypto/Ed25519Signature2020.cjs');
require('@stablelib/ed25519');
require('../types/public-keys/public-key.cjs');
require('../types/public-keys/public-key-sr25519-value.cjs');
require('../types/public-keys/public-key-ed25519-value.cjs');
require('../types/public-keys/public-key-secp256k1-value.cjs');
require('../types/public-keys/public-key-x25519-value.cjs');
require('../types/signatures/signature-value.cjs');
require('../types/signatures/signature.cjs');
require('../utils/types/array.cjs');
require('varint');
require('../utils/encoding/ss-58.cjs');
require('../keypairs/dock-keypair.cjs');
var vc_crypto_JsonWebSignature2020 = require('./crypto/JsonWebSignature2020.cjs');
require('./crypto/common/CustomLinkedDataSignature.cjs');
require('@digitalcredentials/vc-status-list');
require('base64url');
require('./CredentialIssuancePurpose.cjs');
require('../types/did/onchain/typed-did/index.cjs');
require('../types/did/onchain/typed-did/did-method-key/did-method-key-public-key.cjs');
require('../types/generic/typed-enum.cjs');
require('../utils/types/iterable.cjs');
require('../utils/types/object.cjs');
require('../utils/interfaces.cjs');
require('../utils/assert.cjs');
require('../utils/error.cjs');
require('../utils/inheritance.cjs');
require('../utils/types/set.cjs');
require('../types/generic/with-base.cjs');
require('../utils/types/bytes.cjs');
require('../utils/types/string.cjs');
require('../types/generic/with-catch-null.cjs');
require('../types/generic/with-eq.cjs');
require('../types/generic/any-of.cjs');
require('../types/generic/with-from.cjs');
require('blake2b');
require('../types/generic/with-from-json-bytes.cjs');
require('../types/generic/typed-null.cjs');
require('../types/generic/create-placeholder.cjs');
require('../types/generic/typed-bytes.cjs');
require('../types/generic/typed-string.cjs');
require('../types/generic/typed-array.cjs');
require('../types/generic/typed-tuple.cjs');
require('../utils/encoding/base-58-btc.cjs');
require('../types/generic/sized.cjs');
require('../utils/types/number.cjs');
require('../types/generic/typed-number.cjs');
require('../types/generic/typed-set.cjs');
require('../types/generic/typed-map.cjs');
require('../types/generic/typed-struct.cjs');
require('uuid');
require('js-sha256');
require('../types/generic/with-qualifier.cjs');
require('../types/did/onchain/constants.cjs');
require('../types/did/onchain/typed-did/signature.cjs');
require('../types/did/onchain/typed-did/did-method-key/did-method-key-signature.cjs');
require('../types/signatures/signature-ed25519-value.cjs');
require('../types/signatures/signature-secp256k1-value.cjs');
require('../types/signatures/signature-sr25519-value.cjs');
require('../types/did/onchain/typed-did/dock-did-value.cjs');
require('../types/generic/typed-uuid.cjs');
require('../types/generic/utils.cjs');
require('../types/generic/typed-bytes-array.cjs');
require('../types/did/onchain/typed-did/cheqd-did.cjs');
require('@transmute/json-web-signature');
require('./contexts.cjs');
require('./contexts/vc-examples-v1.cjs');
require('./contexts/security_context.cjs');
require('./contexts/status-list-21.cjs');
require('./contexts/private-status-list-21.cjs');
require('../utils/json-fetch.cjs');
require('./crypto/common/DockCryptoKeyPair.cjs');
require('@docknetwork/crypto-wasm-ts/lib/index.js');
require('json-stringify-deterministic');
require('semver/preload.js');
require('../types/did/document/verification-method-ref.cjs');
require('./jws.cjs');
require('./crypto/common/DockCryptoSignatureProof.cjs');
require('./crypto/EcdsaSecp256k1VerificationKey2019.cjs');
require('../utils/encoding/multibase.cjs');
require('elliptic');
require('./crypto/Ed25519VerificationKey2018.cjs');
require('../keypairs/keypair-ed25519.cjs');
require('./crypto/Ed25519VerificationKey2020.cjs');
require('../keypairs/did-keypair.cjs');
require('../types/did/onchain/did-key.cjs');
require('../types/offchain-signatures/params/index.cjs');
require('../types/offchain-signatures/params/value.cjs');
require('../types/generic/option.cjs');
require('../types/offchain-signatures/curve-type.cjs');
require('../types/offchain-signatures/params/id.cjs');
require('../types/generic/with-from-dock-id.cjs');
require('../types/offchain-signatures/public-keys/index.cjs');
require('../types/generic/with-prop.cjs');
require('../types/generic/with-props.cjs');
require('../types/offchain-signatures/public-keys/value.cjs');
require('../types/offchain-signatures/public-keys/ref.cjs');
require('../types/offchain-signatures/public-keys/id.cjs');
require('../types/did/onchain/verification-relationship.cjs');
require('../types/did/document/verification-method-ref-or-ident-ref.cjs');
require('../types/did/document/ident-ref.cjs');
require('./schema.cjs');
require('jsonschema');
require('./revocation.cjs');
require('blakejs');
require('../utils/ident/ensure-ident.cjs');
require('../utils/ident/matchers.cjs');

function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }

var jsonld__default = /*#__PURE__*/_interopDefault(jsonld);
var jsigs__default = /*#__PURE__*/_interopDefault(jsigs);
var b58__default = /*#__PURE__*/_interopDefault(b58);

const { AuthenticationProofPurpose } = jsigs__default.default.purposes;

/**
 * @typedef {object} VerifiablePresentation Representation of a Verifiable Presentation.
 */

/**
 * @param {object} presentation - An object that could be a presentation.
 * @throws {Error}
 * @private
 */
function checkPresentation(presentation) {
  // Normalize to an array to allow the common case of context being a string
  const context = Array.isArray(presentation['@context'])
    ? presentation['@context']
    : [presentation['@context']];

  // Ensure first context is 'https://www.w3.org/2018/credentials/v1'
  if (context[0] !== vc_constants.DEFAULT_CONTEXT_V1_URL) {
    throw new Error(
      `"${vc_constants.DEFAULT_CONTEXT_V1_URL}" needs to be first in the `
        + 'list of contexts.',
    );
  }

  // Ensure VerifiablePresentation exists in types
  const types = jsonld__default.default.getValues(presentation, 'type');
  if (!types.includes('VerifiablePresentation')) {
    throw new Error('"type" must include "VerifiablePresentation".');
  }
}

const DELEGATION_TYPE_URIS = new Set([
  'DelegationCredential',
  'https://ld.truvera.io/credentials/delegation#DelegationCredential',
]);

async function optionalDelegationValidation(
  {
    verified,
    presentation,
    ...rest
  },
  verificationOptions,
  delegationOptions = {},
) {
  const result = {
    ...rest,
    verified,
  };

  if (!presentation || !verified) {
    return result;
  }

  if (!shouldRunDelegationValidation(presentation, delegationOptions)) {
    return result;
  }

  try {
    const expandedPresentation = await jsonld__default.default.expand(presentation, {
      documentLoader: verificationOptions.documentLoader,
    });
    const credentialContexts = buildCredentialContextMap(presentation);
    const cedarOptions = delegationOptions.cedar ?? {};
    const { policies } = cedarOptions;
    const delegationResult = await vcDelegationEngine.verifyVPWithDelegation({
      expandedPresentation,
      credentialContexts,
      documentLoader: verificationOptions.documentLoader,
      failOnUnauthorizedClaims: delegationOptions.failOnUnauthorizedClaims ?? true,
      cedar: cedarOptions.cedar,
      policies,
      delegationPolicy: delegationOptions.delegationPolicy,
    });

    const delegationVerified = delegationResult.decision === 'allow';
    if (delegationVerified && policies) {
      const authorizationOutcome = vcDelegationEngine.authorizeEvaluationsWithCedar({
        cedar: cedarOptions.cedar,
        evaluations: delegationResult.evaluations,
        policies,
      });

      if (authorizationOutcome.decision !== 'allow') {
        throw new Error('Cedar policy evaluation denied');
      }
    }

    return {
      ...result,
      delegationResult,
      verified: result.verified && delegationVerified,
    };
  } catch (error) {
    return {
      ...result,
      delegationResult: {
        decision: 'deny',
        failures: [
          {
            code: 'DELEGATION_VALIDATION_ERROR',
            message: error?.message ?? 'Delegation or policy validation failed',
            stack: error?.stack,
          },
        ],
      },
      verified: false,
      error,
    };
  }
}

function shouldRunDelegationValidation(presentation, delegationOptions = {}) {
  const cedarOptions = delegationOptions.cedar;
  if (cedarOptions?.policies) {
    return true;
  }
  return presentationContainsDelegationCredential(presentation);
}

function presentationContainsDelegationCredential(presentation) {
  const credentials = jsonld__default.default.getValues(presentation, 'verifiableCredential') ?? [];
  return credentials.some((credential) => hasDelegationCredentialType(credential));
}

function hasDelegationCredentialType(credential) {
  if (!credential) {
    return false;
  }
  const rawTypes = credential.type ?? credential['@type'];
  let types = [];
  if (Array.isArray(rawTypes)) {
    types = rawTypes;
  } else if (rawTypes) {
    types = [rawTypes];
  }
  return types.some(
    (type) => typeof type === 'string'
      && (DELEGATION_TYPE_URIS.has(type) || type.endsWith('#DelegationCredential')),
  );
}

function buildCredentialContextMap(presentation) {
  const contexts = new Map();
  const credentials = jsonld__default.default.getValues(presentation, 'verifiableCredential') ?? [];
  credentials.forEach((credential) => {
    const { id } = credential;
    if (typeof id === 'string' && credential['@context']) {
      contexts.set(id, credential['@context']);
    }
  });
  return contexts;
}

async function verifyPresentationCredentials(
  presentation,
  options = {},
) {
  let verified = true;
  let credentialResults = [];

  // Get presentation credentials
  const credentials = jsonld__default.default.getValues(presentation, 'verifiableCredential');
  if (credentials.length > 0) {
    // Verify all credentials in list
    credentialResults = await Promise.all(
      credentials.map((credential) => vc_credentials.verifyCredential(credential, { ...options })),
    );

    // Assign credentialId property to all credential results
    for (const [i, credentialResult] of credentialResults.entries()) {
      credentialResult.credentialId = credentials[i].id;
    }

    // Check all credentials passed verification
    const allCredentialsVerified = credentialResults.every((r) => r.verified);
    if (!allCredentialsVerified) {
      verified = false;
    }
  }

  return {
    verified,
    credentialResults,
  };
}

/**
 * @typedef {object} VerifiableParams The Options to verify credentials and presentations.
 * @property {string} [challenge] - proof challenge Required.
 * @property {string} [domain] - proof domain (optional)
 * @property {string} [controller] - controller (optional)
 * @property {DIDResolver} [resolver] - Resolver to resolve the issuer DID (optional)
 * @property {Boolean} [unsignedPresentation] - Whether to verify the proof or not
 * @property {Boolean} [compactProof] - Whether to compact the JSON-LD or not.
 * @property {object} [presentationPurpose] - A purpose other than the default AuthenticationProofPurpose
 * @property {object} [documentLoader] - A document loader, can be null and use the default (built from `resolver` when omitted). Must resolve each `delegationPolicyId` to the policy JSON when credentials reference a delegation policy.
 * @property {boolean} [failOnUnauthorizedClaims] - When true, delegation chains that emit unauthorized claims cause verification to fail
 * @property {object} [policies] - Optional delegation policy bundle forwarded to @docknetwork/vc-delegation-engine
 * @property {object} [cedarAuth] - Optional cedar authorization configuration forwarded to @docknetwork/vc-delegation-engine
 * @property {object} [delegationPolicy] - Optional: set `enabled: false` to skip delegation policy digest/schema/chain checks even when credentials carry `delegationPolicyId` / `delegationPolicyDigest`.
 */

/**
 * Verify a Verifiable Presentation. Returns the verification status and error in an object
 * @param {object} presentation The verifiable presentation
 * @param {VerifiableParams} options Verify parameters, this object is passed down to jsonld-signatures calls
 * @return {Promise<object>} verification result. The returned object will have a key `verified` which is true if the
 * presentation is valid and all the credentials are valid and not revoked and false otherwise. The `error` will
 * describe the error if any.
 */
async function verifyPresentation(presentation, options = {}) {
  if (options.documentLoader && options.resolver) {
    throw new Error(
      'Passing resolver and documentLoader results in resolver being ignored, please re-factor.',
    );
  }

  // Ensure presentation is passed
  if (!presentation) {
    throw new TypeError('"presentation" property is required');
  }

  if (isAnoncreds(presentation)) {
    return verifyAnoncreds(presentation, options);
  }

  // Ensure presentation is valid
  checkPresentation(presentation);

  // Extract parameters
  const {
    challenge,
    domain,
    resolver,
    unsignedPresentation = false,
    presentationPurpose,
    controller,
    suite = [],
  } = options;
  const delegationOptions = {
    failOnUnauthorizedClaims: options.failOnUnauthorizedClaims,
    cedar: options.cedarAuth,
    delegationPolicy: options.delegationPolicy,
  };

  // Build verification options
  const verificationOptions = {
    documentLoader: options.documentLoader || vc_documentLoader(resolver),
    ...options,
    resolver: null,
    suite: [
      new vc_crypto_Ed25519Signature2018(),
      new vc_crypto_Ed25519Signature2020(),
      new vc_crypto_EcdsaSecp256k1Signature2019(),
      // new Sr25519Signature2020(),
      new vc_crypto_JsonWebSignature2020(),
      ...suite,
    ],
  };

  // TODO: verify proof then credentials
  const { verified, credentialResults } = await verifyPresentationCredentials(
    presentation,
    verificationOptions,
  );
  try {
    // Early out incase credentials arent verified
    if (!verified) {
      return { verified, results: [presentation], credentialResults };
    }

    // Skip proof validation for unsigned
    if (unsignedPresentation) {
      return await optionalDelegationValidation(
        {
          verified,
          presentation,
          results: [presentation],
          credentialResults,
        },
        verificationOptions,
        delegationOptions,
      );
    }

    // Get proof purpose
    if (!presentationPurpose && !challenge) {
      throw new Error(
        'A "challenge" param is required for AuthenticationProofPurpose.',
      );
    }

    // Set purpose and verify
    const purpose = presentationPurpose
      || new AuthenticationProofPurpose({ controller, domain, challenge });
    const presentationResult = await jsigs__default.default.verify(presentation, {
      purpose,
      ...verificationOptions,
    });

    // Return results
    return await optionalDelegationValidation({
      presentation,
      presentationResult,
      credentialResults,
      verified: verified && presentationResult.verified,
      error: presentationResult.error,
    }, verificationOptions, delegationOptions);
  } catch (error) {
    // Error occured when verifying presentation, catch and return error
    return {
      verified: false,
      results: [{ verified: false, error }],
      error,
    };
  }
}
/**
 * Sign a Verifiable Presentation
 * @param {object} presentation - the one to be signed
 * @param {object} keyDoc - key document containing `id`, `controller`, `type`, `privateKeyBase58` and `publicKeyBase58`
 * @param {string} challenge - proof challenge Required.
 * @param {string} domain - proof domain (optional)
 * @param {DIDResolver} [resolver] - Resolver for DIDs.
 * @param {Boolean} [compactProof] - Whether to compact the JSON-LD or not.
 * @param {object} [presentationPurpose] - Optional presentation purpose to override default AuthenticationProofPurpose
 * @return {Promise<VerifiablePresentation>} A VerifiablePresentation with a proof.
 */
async function signPresentation(
  presentation,
  keyDoc,
  challenge,
  domain,
  resolver = null,
  compactProof = true,
  presentationPurpose = null,
  addSuiteContext = true,
) {
  const suite = await vc_helpers.getSuiteFromKeyDoc(keyDoc);
  const purpose = presentationPurpose
    || new AuthenticationProofPurpose({
      domain,
      challenge,
    });

  const documentLoader = vc_documentLoader(resolver);
  const signed = await jsigs__default.default.sign(presentation, {
    purpose,
    documentLoader,
    domain,
    challenge,
    compactProof,
    suite,
    addSuiteContext,
  });

  // Sometimes jsigs returns proof like [null, { proof }]
  // check for that case here and if there's only 1 proof store object instead
  if (Array.isArray(signed.proof)) {
    const validProofs = signed.proof.filter((p) => !!p);
    if (validProofs.length === 1) {
      signed.proof = validProofs.pop();
    }
  }
  return signed;
}

function isAnoncreds(presentation) {
  // Since there is no type parameter present we have to guess by checking field types
  // these wont exist in a standard VP
  return (
    typeof presentation.version === 'string'
    && typeof presentation.proof === 'string'
    && typeof presentation.spec !== 'undefined'
    && typeof presentation.spec.credentials !== 'undefined'
  );
}

/**
 * Verify an anoncreds presentation given in JSON format
 * @param presentation - object
 * @param options - Any accumulator public keys, predicate params, etc required to verify the presentation.
 * @returns {Promise<VerifyResult>}
 */
async function verifyAnoncreds(presentation, options = {}) {
  const documentLoader = options.documentLoader || vc_documentLoader(options.resolver);
  const {
    predicateParams,
    accumulatorPublicKeys,
    circomOutputs,
    blindedAttributesCircomOutputs,
  } = options;

  const keyDocuments = await Promise.all(
    presentation.spec.credentials.map((c, idx) => {
      const { proof } = c.revealedAttributes;
      if (!proof) {
        throw new Error(
          `Presentation credential does not reveal its proof for index ${idx}`,
        );
      }

      let sigClass;
      switch (proof.type) {
        case vc_crypto_constants.Bls12381BBSSigDockSigName:
          sigClass = vc_crypto_Bls12381BBSSignatureDock2022;
          break;
        case vc_crypto_constants.Bls12381BBS23SigDockSigName:
          sigClass = vc_crypto_Bls12381BBSSignatureDock2023;
          break;
        case vc_crypto_constants.Bls12381PSSigDockSigName:
          sigClass = vc_crypto_Bls12381PSSignatureDock2023;
          break;
        case vc_crypto_constants.Bls12381BBDT16MacDockName:
          return { type: vc_crypto_constants.Bls12381BBDT16DockVerKeyName };
        default:
          throw new Error(`Invalid proof type ${proof.type}`);
      }

      return sigClass.getVerificationMethod({ proof, documentLoader });
    }),
  );

  const recreatedPres = cryptoWasmTs.Presentation.fromJSON(presentation);
  const pks = new Map();

  keyDocuments.forEach((keyDocument, i) => {
    if (!keyDocument.type) {
      throw new Error(
        `No type provided for key document ${JSON.stringify(keyDocument)}`,
      );
    }

    // Question: Why would keyDocument.type start with `did:`
    const keyType = keyDocument.type.startsWith('did:')
      ? keyDocument.type.slice(4)
      : keyDocument.type;
    let Cls;

    switch (keyType) {
      case vc_crypto_constants.Bls12381BBSDockVerKeyName:
        Cls = cryptoWasmTs.BBSPlusPublicKeyG2;
        break;
      case vc_crypto_constants.Bls12381BBS23DockVerKeyName:
        Cls = cryptoWasmTs.BBSPublicKey;
        break;
      case vc_crypto_constants.Bls12381PSDockVerKeyName:
        Cls = cryptoWasmTs.PSPublicKey;
        break;
      case vc_crypto_constants.Bls12381BBDT16DockVerKeyName:
        return;
      default:
        throw new Error(`Invalid key document type: ${keyType}`);
    }

    const pkRaw = b58__default.default.decode(keyDocument.publicKeyBase58);
    pks.set(i, new Cls(pkRaw));
  });

  return recreatedPres.verify(
    pks,
    accumulatorPublicKeys,
    predicateParams,
    circomOutputs,
    blindedAttributesCircomOutputs,
  );
}

/**
 * Get the keyed proofs from the given anoncreds presentation. This is supposed to be called by the verifier to
 * get the keyed-proofs and send them for verification to the issuer or the entity having the corresponding secret key
 * @param presentation
 * @returns {Map<number, KeyedProof>}
 */
function getKeyedProofsFromVerifiedPresentation(presentation) {
  if (!isAnoncreds(presentation)) {
    throw new Error('Only anoncreds presentation supported');
  }
  const presObject = cryptoWasmTs.Presentation.fromJSON(presentation);
  return presObject.getKeyedProofs();
}

/**
 * Get JSON-schemas of all credentials in the presentation
 * @param presentation
 * @param full - when set to true, returns the JSON schema of each credential with properties. This might be a fetched schema
 * @returns {*}
 */
function getJsonSchemasFromPresentation(presentation, full = false) {
  return presentation.spec.credentials.map((cred) => {
    const schema = vc_crypto_common_DockCryptoSignature.isCredVerGte060(cred.version)
      ? cryptoWasmTs.CredentialSchema.fromJSON(cred.schema)
      : cryptoWasmTs.CredentialSchema.fromJSON(JSON.parse(cred.schema));
    return full ? schema.getEmbeddedJsonSchema() : schema.jsonSchema;
  });
}

/**
 * Get status of all credentials from the presentation with revocation type of private status list.
 * @param presentation
 * @returns {Object[]}
 */
function getPrivateStatuses(presentation) {
  const credentials = jsonld__default.default.getValues(presentation, 'verifiableCredential');
  return credentials.map((c) => vc_credentials.getPrivateStatus(c));
}

exports.getJsonSchemasFromPresentation = getJsonSchemasFromPresentation;
exports.getKeyedProofsFromVerifiedPresentation = getKeyedProofsFromVerifiedPresentation;
exports.getPrivateStatuses = getPrivateStatuses;
exports.isAnoncreds = isAnoncreds;
exports.signPresentation = signPresentation;
exports.verifyAnoncreds = verifyAnoncreds;
exports.verifyPresentation = verifyPresentation;
exports.verifyPresentationCredentials = verifyPresentationCredentials;
//# sourceMappingURL=presentations.cjs.map
