import { createHash } from "node:crypto";

import type { TmpaGovernanceObject } from "@codeflowmu/protocol";

export const CODEFLOWMU_TMPA_S06_CANONICALIZATION = "codeflowmu-tmpa-s0.6-json-v1";
export const CODEFLOWMU_TMPA_CANONICALIZATION = "codeflowmu-tmpa-s1.0-json-v1";

export const CODEFLOWMU_TMPA_CANONICALIZATION_PROFILE = {
  id: CODEFLOWMU_TMPA_CANONICALIZATION,
  version: "1.0.0",
  character_encoding: "UTF-8",
  unicode_normalization: "none",
  line_ending: "LF",
  object_key_order: "Unicode code-point order",
  array_order:
    "preserved unless the governing clause declares the collection unordered",
  json_serialization:
    "JSON.stringify with no insignificant whitespace and one trailing LF",
  number_representation: "ECMAScript JSON.stringify",
  timestamp_representation:
    "RFC 3339 date-time accepted by AJV Draft 2020-12 format assertion",
  integrity_field_treatment: {
    digest: "replaced with the empty string before hashing",
    signature_algorithm: "excluded before hashing",
    key_id: "excluded before hashing",
    signature: "excluded before hashing",
  },
  extension_field_treatment:
    "included recursively in Unicode code-point object-key order",
  locale_sensitive_collation: false,
} as const;

export const CODEFLOWMU_TMPA_S06_CANONICALIZATION_PROFILE = {
  ...CODEFLOWMU_TMPA_CANONICALIZATION_PROFILE,
  id: CODEFLOWMU_TMPA_S06_CANONICALIZATION,
} as const;

export function compareUnicodeCodePoints(
  leftValue: unknown,
  rightValue: unknown,
): number {
  const left = [...String(leftValue)];
  const right = [...String(rightValue)];
  for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
    if (left[index] === undefined) return -1;
    if (right[index] === undefined) return 1;
    const difference =
      (left[index]?.codePointAt(0) ?? 0) - (right[index]?.codePointAt(0) ?? 0);
    if (difference) return difference;
  }
  return 0;
}

export function stableValue<T>(value: T): T {
  if (Array.isArray(value)) {
    return value.map((item) => stableValue(item)) as T;
  }
  if (value && typeof value === "object") {
    const record = value as Record<string, unknown>;
    return Object.fromEntries(
      Object.keys(record)
        .sort(compareUnicodeCodePoints)
        .map((key) => [key, stableValue(record[key])]),
    ) as T;
  }
  return value;
}

export function canonicalJson(value: unknown): string {
  return `${JSON.stringify(stableValue(value))}\n`;
}

export function canonicalBytes(value: unknown): Buffer {
  return Buffer.from(canonicalJson(value), "utf8");
}

export function sha256Digest(value: Uint8Array | string): string {
  return `sha256:${createHash("sha256").update(value).digest("hex")}`;
}

export function tmpaObjectDigest(object: TmpaGovernanceObject): string {
  const covered = structuredClone(object);
  covered.integrity = { ...covered.integrity, digest: "" };
  delete covered.integrity.signature_algorithm;
  delete covered.integrity.key_id;
  delete covered.integrity.signature;
  return sha256Digest(canonicalBytes(covered));
}

export function finalizeTmpaObject(
  object: Omit<TmpaGovernanceObject, "integrity"> & {
    integrity?: Partial<TmpaGovernanceObject["integrity"]>;
  },
): TmpaGovernanceObject {
  const value = structuredClone(object) as TmpaGovernanceObject;
  const supplied = value.integrity ?? ({} as TmpaGovernanceObject["integrity"]);
  value.integrity = {
    ...supplied,
    canonicalization:
      supplied.canonicalization ||
      (value.tmpa_version === "S0.6"
        ? CODEFLOWMU_TMPA_S06_CANONICALIZATION
        : value.tmpa_version === "S0.5"
          ? "codeflowmu-tmpa-s0.5-json-v1"
          : CODEFLOWMU_TMPA_CANONICALIZATION),
    hash_algorithm: supplied.hash_algorithm || "sha256",
    digest: supplied.digest || "",
  };
  value.integrity.digest = tmpaObjectDigest(value);
  return value;
}
