<?php

declare(strict_types=1);

namespace Drupal\flowdrop\Service\Bundle;

/**
 * Produces a deterministic byte representation of a bundle payload.
 *
 * A workflow-bundle signature is computed over these exact bytes, so the bytes
 * must be reproducible regardless of how the source JSON happened to be
 * ordered or whitespaced. Two payloads that are semantically equal (same keys,
 * same values, different key order or formatting) MUST canonicalize to the
 * identical string; any change to a value MUST change the output.
 *
 * Rules:
 * - Object (associative array) keys are sorted ascending, recursively.
 * - List arrays preserve their order (order is meaningful for nodes/edges).
 * - Output is compact UTF-8 JSON with no insignificant whitespace, slashes and
 *   unicode left unescaped so the bytes are stable across encoders.
 *
 * This is the security-critical primitive of the bundle transport: signing and
 * verification both route through it, so "verify one thing, import another" is
 * impossible by construction.
 *
 * @internal
 */
final class BundleCanonicalizer {

  /**
   * Canonicalizes decoded JSON data to a deterministic byte string.
   *
   * @param mixed $data
   *   Decoded JSON data (scalars, lists, or associative arrays).
   *
   * @return string
   *   The canonical UTF-8 JSON representation.
   */
  public function canonicalize(mixed $data): string {
    $encoded = json_encode(
      $this->normalize($data),
      JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR,
    );

    return $encoded;
  }

  /**
   * Recursively sorts associative-array keys while preserving list order.
   *
   * @param mixed $data
   *   The data to normalize.
   *
   * @return mixed
   *   The normalized data, ready for deterministic encoding.
   */
  private function normalize(mixed $data): mixed {
    if (!is_array($data)) {
      return $data;
    }

    if (array_is_list($data)) {
      return array_map(fn ($value) => $this->normalize($value), $data);
    }

    // Associative array: sort by key, then normalize each value.
    ksort($data);
    foreach ($data as $key => $value) {
      $data[$key] = $this->normalize($value);
    }

    return $data;
  }

}
