import type { EventPublishOptions } from "./index.js";
import type { EventTransportValue } from "./transport.js";

const PARSED_EVENT_PAYLOAD = Symbol.for("beignet.eventPayload.parsed");
const TRANSPORT_STABLE_EVENT_PAYLOAD = Symbol.for(
  "beignet.eventPayload.transportStable",
);

type ParsedEventPublishOptions = EventPublishOptions & {
  [PARSED_EVENT_PAYLOAD]?: object;
  [TRANSPORT_STABLE_EVENT_PAYLOAD]?: object;
};

interface ParsedPayloadState {
  readonly eventName: string;
  readonly payloadSchema: object;
  readonly validate: unknown;
  readonly payload: unknown;
}

type PayloadSnapshot =
  | { readonly kind: "primitive"; readonly value: unknown }
  | {
      readonly kind: "array";
      readonly value: object;
      readonly prototype: object | null;
      readonly items: readonly PayloadSnapshot[];
    }
  | {
      readonly kind: "object";
      readonly value: object;
      readonly prototype: object | null;
      readonly entries: readonly (readonly [string, PayloadSnapshot])[];
    };

interface TransportStablePayloadState {
  readonly eventName: string;
  readonly payloadSchema: object;
  readonly validate: unknown;
  readonly payload: PayloadSnapshot;
  readonly fingerprint: string;
}

interface EventPayloadIdentity {
  readonly name: string;
  readonly payload: {
    readonly "~standard": {
      readonly validate: unknown;
    };
  };
}

const transportStablePayloadStates = new WeakMap<
  object,
  TransportStablePayloadState
>();
const parsedPayloadStates = new WeakMap<object, ParsedPayloadState>();

function transportFingerprint(value: EventTransportValue): string {
  return JSON.stringify(value);
}

function capturePayloadSnapshot(value: unknown): PayloadSnapshot {
  if (value === null || typeof value !== "object") {
    return { kind: "primitive", value };
  }
  if (Array.isArray(value)) {
    const items: PayloadSnapshot[] = [];
    for (let index = 0; index < value.length; index += 1) {
      const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
      if (!descriptor || !("value" in descriptor)) {
        throw new TypeError(
          "Transport-stable array state must contain data properties.",
        );
      }
      items.push(capturePayloadSnapshot(descriptor.value));
    }
    return {
      kind: "array",
      value,
      prototype: Object.getPrototypeOf(value),
      items,
    };
  }
  return {
    kind: "object",
    value,
    prototype: Object.getPrototypeOf(value),
    entries: Reflect.ownKeys(value).map((key) => {
      const descriptor = Object.getOwnPropertyDescriptor(value, key);
      if (typeof key !== "string" || !descriptor || !("value" in descriptor)) {
        throw new TypeError(
          "Transport-stable payload state must contain data properties.",
        );
      }
      return [key, capturePayloadSnapshot(descriptor.value)] as const;
    }),
  };
}

function matchesPayloadSnapshot(
  snapshot: PayloadSnapshot,
  value: unknown,
): boolean {
  if (snapshot.kind === "primitive") return Object.is(snapshot.value, value);
  if (snapshot.value !== value || value === null || typeof value !== "object") {
    return false;
  }
  if (Object.getPrototypeOf(value) !== snapshot.prototype) return false;
  if (snapshot.kind === "array") {
    return (
      Array.isArray(value) &&
      value.length === snapshot.items.length &&
      snapshot.items.every((item, index) => {
        const descriptor = Object.getOwnPropertyDescriptor(
          value,
          String(index),
        );
        return (
          descriptor !== undefined &&
          "value" in descriptor &&
          matchesPayloadSnapshot(item, descriptor.value)
        );
      })
    );
  }
  if (Array.isArray(value)) return false;
  const keys = Reflect.ownKeys(value);
  return (
    keys.length === snapshot.entries.length &&
    snapshot.entries.every(([key, item], index) => {
      const descriptor = Object.getOwnPropertyDescriptor(value, key);
      return (
        keys[index] === key &&
        descriptor !== undefined &&
        "value" in descriptor &&
        matchesPayloadSnapshot(item, descriptor.value)
      );
    })
  );
}

export function markEventPayloadParsed(
  event: EventPayloadIdentity,
  payload: unknown,
  options?: EventPublishOptions,
): EventPublishOptions {
  const token = {};
  parsedPayloadStates.set(token, {
    eventName: event.name,
    payloadSchema: event.payload,
    validate: event.payload["~standard"].validate,
    payload,
  });
  return {
    ...options,
    [PARSED_EVENT_PAYLOAD]: token,
  } as ParsedEventPublishOptions;
}

export function isEventPayloadParsed(
  event: EventPayloadIdentity,
  payload: unknown,
  options?: EventPublishOptions,
): boolean {
  const token = (options as ParsedEventPublishOptions | undefined)?.[
    PARSED_EVENT_PAYLOAD
  ];
  const state = token ? parsedPayloadStates.get(token) : undefined;
  return (
    state?.eventName === event.name &&
    state.payloadSchema === event.payload &&
    state.validate === event.payload["~standard"].validate &&
    Object.is(state.payload, payload)
  );
}

export function markEventPayloadTransportStable(
  event: EventPayloadIdentity,
  payload: unknown,
  transportValue: EventTransportValue,
  options?: EventPublishOptions,
): EventPublishOptions {
  const parsedToken = {};
  parsedPayloadStates.set(parsedToken, {
    eventName: event.name,
    payloadSchema: event.payload,
    validate: event.payload["~standard"].validate,
    payload,
  });
  const stableToken = {};
  transportStablePayloadStates.set(stableToken, {
    eventName: event.name,
    payloadSchema: event.payload,
    validate: event.payload["~standard"].validate,
    payload: capturePayloadSnapshot(payload),
    fingerprint: transportFingerprint(transportValue),
  });
  return {
    ...options,
    [PARSED_EVENT_PAYLOAD]: parsedToken,
    [TRANSPORT_STABLE_EVENT_PAYLOAD]: stableToken,
  } as ParsedEventPublishOptions;
}

export function isEventPayloadTransportStable(
  event: EventPayloadIdentity,
  payload: unknown,
  transportValue: EventTransportValue,
  options?: EventPublishOptions,
): boolean {
  const token = (options as ParsedEventPublishOptions | undefined)?.[
    TRANSPORT_STABLE_EVENT_PAYLOAD
  ];
  const state = token ? transportStablePayloadStates.get(token) : undefined;
  return (
    state?.eventName === event.name &&
    state.payloadSchema === event.payload &&
    state.validate === event.payload["~standard"].validate &&
    matchesPayloadSnapshot(state.payload, payload) &&
    state.fingerprint === transportFingerprint(transportValue)
  );
}
