import jsonld from 'jsonld';
import jsigs from 'jsonld-signatures';
import { CredentialSchema, Presentation, PSPublicKey, BBSPublicKey, BBSPlusPublicKeyG2 } from '@docknetwork/crypto-wasm-ts';
import b58 from 'bs58';
import { verifyVPWithDelegation, authorizeEvaluationsWithCedar } from '@docknetwork/vc-delegation-engine';
import { getPrivateStatus, verifyCredential } from './credentials.js';
import { isCredVerGte060 } from './crypto/common/DockCryptoSignature.js';
import documentLoader from './document-loader.js';
import { getSuiteFromKeyDoc } from './helpers.js';
import { Bls12381BBDT16MacDockName, Bls12381BBDT16DockVerKeyName, Bls12381PSSigDockSigName, Bls12381BBS23SigDockSigName, Bls12381BBSSigDockSigName, Bls12381PSDockVerKeyName, Bls12381BBS23DockVerKeyName, Bls12381BBSDockVerKeyName } from './crypto/constants.js';
import { DEFAULT_CONTEXT_V1_URL } from './constants.js';
import './crypto/Bls12381BBDT16KeyPairDock2024.js';
import './crypto/Bls12381BBDT16MACDock2024.js';
import './crypto/Bls12381BBDT16MACProofDock2024.js';
import './crypto/Bls12381BBSKeyPairDock2023.js';
import Bls12381BBSSignatureDock2022 from './crypto/Bls12381BBSSignatureDock2022.js';
import Bls12381BBSSignatureDock2023 from './crypto/Bls12381BBSSignatureDock2023.js';
import './crypto/Bls12381BBSSignatureProofDock2022.js';
import './crypto/Bls12381BBSSignatureProofDock2023.js';
import './crypto/Bls12381BDDT16KeyPairDock2024.js';
import './crypto/Bls12381BDDT16MACDock2024.js';
import './crypto/Bls12381BDDT16MACProofDock2024.js';
import './crypto/Bls12381G2KeyPairDock2022.js';
import './crypto/Bls12381PSKeyPairDock2023.js';
import Bls12381PSSignatureDock2023 from './crypto/Bls12381PSSignatureDock2023.js';
import './crypto/Bls12381PSSignatureProofDock2023.js';
import EcdsaSecp256k1Signature2019 from './crypto/EcdsaSecp256k1Signature2019.js';
import '@juanelas/base64';
import 'crypto';
import '../keypairs/keypair-secp256k1.js';
import 'multiformats/bases/base58';
import Ed25519Signature2018 from './crypto/Ed25519Signature2018.js';
import Ed25519Signature2020 from './crypto/Ed25519Signature2020.js';
import '@stablelib/ed25519';
import '../types/public-keys/public-key.js';
import '../types/public-keys/public-key-sr25519-value.js';
import '../types/public-keys/public-key-ed25519-value.js';
import '../types/public-keys/public-key-secp256k1-value.js';
import '../types/public-keys/public-key-x25519-value.js';
import '../types/signatures/signature-value.js';
import '../types/signatures/signature.js';
import '../utils/types/array.js';
import 'varint';
import '../utils/encoding/ss-58.js';
import '../keypairs/dock-keypair.js';
import JsonWebSignature2020 from './crypto/JsonWebSignature2020.js';
import './crypto/common/CustomLinkedDataSignature.js';
import '@digitalcredentials/vc-status-list';
import 'base64url';
import './CredentialIssuancePurpose.js';
import '../types/did/onchain/typed-did/index.js';
import '../types/did/onchain/typed-did/did-method-key/did-method-key-public-key.js';
import '../types/generic/typed-enum.js';
import '../utils/types/iterable.js';
import '../utils/types/object.js';
import '../utils/interfaces.js';
import '../utils/assert.js';
import '../utils/error.js';
import '../utils/inheritance.js';
import '../utils/types/set.js';
import '../types/generic/with-base.js';
import '../utils/types/bytes.js';
import '../utils/types/string.js';
import '../types/generic/with-catch-null.js';
import '../types/generic/with-eq.js';
import '../types/generic/any-of.js';
import '../types/generic/with-from.js';
import 'blake2b';
import '../types/generic/with-from-json-bytes.js';
import '../types/generic/typed-null.js';
import '../types/generic/create-placeholder.js';
import '../types/generic/typed-bytes.js';
import '../types/generic/typed-string.js';
import '../types/generic/typed-array.js';
import '../types/generic/typed-tuple.js';
import '../utils/encoding/base-58-btc.js';
import '../types/generic/sized.js';
import '../utils/types/number.js';
import '../types/generic/typed-number.js';
import '../types/generic/typed-set.js';
import '../types/generic/typed-map.js';
import '../types/generic/typed-struct.js';
import 'uuid';
import 'js-sha256';
import '../types/generic/with-qualifier.js';
import '../types/did/onchain/constants.js';
import '../types/did/onchain/typed-did/signature.js';
import '../types/did/onchain/typed-did/did-method-key/did-method-key-signature.js';
import '../types/signatures/signature-ed25519-value.js';
import '../types/signatures/signature-secp256k1-value.js';
import '../types/signatures/signature-sr25519-value.js';
import '../types/did/onchain/typed-did/dock-did-value.js';
import '../types/generic/typed-uuid.js';
import '../types/generic/utils.js';
import '../types/generic/typed-bytes-array.js';
import '../types/did/onchain/typed-did/cheqd-did.js';
import '@transmute/json-web-signature';
import './contexts.js';
import './contexts/vc-examples-v1.js';
import './contexts/security_context.js';
import './contexts/status-list-21.js';
import './contexts/private-status-list-21.js';
import '../utils/json-fetch.js';
import './crypto/common/DockCryptoKeyPair.js';
import '@docknetwork/crypto-wasm-ts/lib/index.js';
import 'json-stringify-deterministic';
import 'semver/preload.js';
import '../types/did/document/verification-method-ref.js';
import './jws.js';
import './crypto/common/DockCryptoSignatureProof.js';
import './crypto/EcdsaSecp256k1VerificationKey2019.js';
import '../utils/encoding/multibase.js';
import 'elliptic';
import './crypto/Ed25519VerificationKey2018.js';
import '../keypairs/keypair-ed25519.js';
import './crypto/Ed25519VerificationKey2020.js';
import '../keypairs/did-keypair.js';
import '../types/did/onchain/did-key.js';
import '../types/offchain-signatures/params/index.js';
import '../types/offchain-signatures/params/value.js';
import '../types/generic/option.js';
import '../types/offchain-signatures/curve-type.js';
import '../types/offchain-signatures/params/id.js';
import '../types/generic/with-from-dock-id.js';
import '../types/offchain-signatures/public-keys/index.js';
import '../types/generic/with-prop.js';
import '../types/generic/with-props.js';
import '../types/offchain-signatures/public-keys/value.js';
import '../types/offchain-signatures/public-keys/ref.js';
import '../types/offchain-signatures/public-keys/id.js';
import '../types/did/onchain/verification-relationship.js';
import '../types/did/document/verification-method-ref-or-ident-ref.js';
import '../types/did/document/ident-ref.js';
import './schema.js';
import 'jsonschema';
import './revocation.js';
import 'blakejs';
import '../utils/ident/ensure-ident.js';
import '../utils/ident/matchers.js';

const { AuthenticationProofPurpose } = jsigs.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] !== DEFAULT_CONTEXT_V1_URL) {
    throw new Error(
      `"${DEFAULT_CONTEXT_V1_URL}" needs to be first in the `
        + 'list of contexts.',
    );
  }

  // Ensure VerifiablePresentation exists in types
  const types = jsonld.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.expand(presentation, {
      documentLoader: verificationOptions.documentLoader,
    });
    const credentialContexts = buildCredentialContextMap(presentation);
    const cedarOptions = delegationOptions.cedar ?? {};
    const { policies } = cedarOptions;
    const delegationResult = await 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 = 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.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.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.getValues(presentation, 'verifiableCredential');
  if (credentials.length > 0) {
    // Verify all credentials in list
    credentialResults = await Promise.all(
      credentials.map((credential) => 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 || documentLoader(resolver),
    ...options,
    resolver: null,
    suite: [
      new Ed25519Signature2018(),
      new Ed25519Signature2020(),
      new EcdsaSecp256k1Signature2019(),
      // new Sr25519Signature2020(),
      new 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.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 getSuiteFromKeyDoc(keyDoc);
  const purpose = presentationPurpose
    || new AuthenticationProofPurpose({
      domain,
      challenge,
    });

  const documentLoader$1 = documentLoader(resolver);
  const signed = await jsigs.sign(presentation, {
    purpose,
    documentLoader: documentLoader$1,
    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$1 = options.documentLoader || 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 Bls12381BBSSigDockSigName:
          sigClass = Bls12381BBSSignatureDock2022;
          break;
        case Bls12381BBS23SigDockSigName:
          sigClass = Bls12381BBSSignatureDock2023;
          break;
        case Bls12381PSSigDockSigName:
          sigClass = Bls12381PSSignatureDock2023;
          break;
        case Bls12381BBDT16MacDockName:
          return { type: Bls12381BBDT16DockVerKeyName };
        default:
          throw new Error(`Invalid proof type ${proof.type}`);
      }

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

  const recreatedPres = 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 Bls12381BBSDockVerKeyName:
        Cls = BBSPlusPublicKeyG2;
        break;
      case Bls12381BBS23DockVerKeyName:
        Cls = BBSPublicKey;
        break;
      case Bls12381PSDockVerKeyName:
        Cls = PSPublicKey;
        break;
      case Bls12381BBDT16DockVerKeyName:
        return;
      default:
        throw new Error(`Invalid key document type: ${keyType}`);
    }

    const pkRaw = b58.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 = 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 = isCredVerGte060(cred.version)
      ? CredentialSchema.fromJSON(cred.schema)
      : 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.getValues(presentation, 'verifiableCredential');
  return credentials.map((c) => getPrivateStatus(c));
}

export { getJsonSchemasFromPresentation, getKeyedProofsFromVerifiedPresentation, getPrivateStatuses, isAnoncreds, signPresentation, verifyAnoncreds, verifyPresentation, verifyPresentationCredentials };
//# sourceMappingURL=presentations.js.map
