<?php

declare(strict_types=1);

namespace Drupal\flowdrop\Service\Bundle;

/**
 * Verifies a workflow bundle's detached Ed25519 signature.
 *
 * The signature is computed over the canonical bytes of the bundle's payload
 * (see BundleCanonicalizer). Verification recomputes those bytes from the same
 * payload that will be imported, so a bundle cannot present one payload for
 * signing and a different one for import.
 *
 * Trust is resolved through TrustedPublisherResolverInterface: a valid
 * signature from a publisher that is not enabled/known is reported as
 * untrusted, not trusted.
 *
 * @internal
 */
final class BundleVerifier {

  /**
   * The signature algorithm this verifier accepts.
   */
  public const ALGORITHM = 'ed25519';

  /**
   * The bundle envelope format this version produces and accepts.
   *
   * The format lives outside the signed payload, so it must never be trusted
   * for a security decision — it is only a version discriminator used to
   * reject envelopes this release cannot interpret.
   */
  public const FORMAT = 'flowdrop.bundle/v1';

  public function __construct(
    private readonly BundleCanonicalizer $canonicalizer,
    private readonly TrustedPublisherResolverInterface $publishers,
  ) {}

  /**
   * Verifies a decoded bundle.
   *
   * @param array $bundle
   *   The decoded bundle: expects a 'payload' array and an optional
   *   'signature' array with 'alg', 'key_id', and 'value' (base64) members.
   *
   * @return \Drupal\flowdrop\Service\Bundle\VerificationResult
   *   The verification outcome.
   */
  public function verify(array $bundle): VerificationResult {
    $signature = $bundle['signature'] ?? NULL;
    if (!is_array($signature) || empty($signature['value'])) {
      return VerificationResult::unsigned();
    }

    $alg = $signature['alg'] ?? '';
    if ($alg !== self::ALGORITHM) {
      return VerificationResult::untrusted(
        sprintf('Unsupported signature algorithm "%s".', (string) $alg),
        $signature['key_id'] ?? NULL,
      );
    }

    $keyId = $signature['key_id'] ?? '';
    if ($keyId === '') {
      return VerificationResult::untrusted('Signature names no publisher key id.');
    }

    // The signed payload's declared publisher must match the signature key id;
    // a mismatch is an attempt to attribute a payload to another publisher.
    $payload = $bundle['payload'] ?? NULL;
    if (!is_array($payload)) {
      return VerificationResult::untrusted('Bundle has no payload to verify.', $keyId);
    }
    if (($payload['publisher'] ?? NULL) !== $keyId) {
      return VerificationResult::untrusted(
        'Payload publisher does not match the signature key id.',
        $keyId,
      );
    }

    $publicKey = $this->publishers->getPublicKey($keyId);
    if ($publicKey === NULL) {
      return VerificationResult::untrusted(
        sprintf('Publisher "%s" is not a trusted, enabled publisher on this site.', $keyId),
        $keyId,
      );
    }

    $rawSignature = base64_decode((string) $signature['value'], TRUE);
    if ($rawSignature === FALSE) {
      return VerificationResult::untrusted('Signature value is not valid base64.', $keyId);
    }

    $message = $this->canonicalizer->canonicalize($payload);

    $valid = sodium_crypto_sign_verify_detached($rawSignature, $message, $publicKey);
    if (!$valid) {
      return VerificationResult::untrusted(
        'Signature does not verify against the publisher key.',
        $keyId,
      );
    }

    return VerificationResult::trusted($keyId);
  }

}
