import type { StandardSchemaV1 } from "@standard-schema/spec";
import {
  eventTransportValuesEqual,
  toEventTransportValue,
} from "../events/transport.js";

/** Browser-visible channel contract. Authorization belongs in a server binding. */
export interface ChannelDefinition<
  Name extends string = string,
  Params extends StandardSchemaV1<
    unknown,
    Record<string, string>
  > = StandardSchemaV1<unknown, Record<string, string>>,
  Events extends Record<string, StandardSchemaV1> = Record<
    string,
    StandardSchemaV1
  >,
> {
  readonly kind: "channel";
  readonly name: Name;
  readonly params: Params;
  readonly events: Events;
}

export type InferChannelParams<C extends ChannelDefinition> =
  StandardSchemaV1.InferOutput<C["params"]>;
export type InferChannelEvent<C extends ChannelDefinition> = {
  [Event in keyof C["events"] & string]: {
    event: Event;
    data: StandardSchemaV1.InferOutput<C["events"][Event]>;
  };
}[keyof C["events"] & string];

/** Protocol defaults and safety limits shared by clients, endpoints, and providers. */
export const broadcastLimits = Object.freeze({
  subscriptions: 20,
  requestBytes: 8_192,
  payloadBytes: 65_536,
  bufferedBytes: 1_048_576,
  defaultLifetimeMs: 60_000,
  maxLifetimeMs: 3_600_000,
  lifetimeGraceMs: 5_000,
  heartbeatMs: 25_000,
  readyTimeoutMs: 10_000,
});

/** Invalid channel configuration or untrusted protocol input. */
export class BroadcastValidationError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "BroadcastValidationError";
  }
}

const namePattern = /^[a-zA-Z][a-zA-Z0-9_.-]{0,127}$/;

export function defineChannel<
  const Name extends string,
  Params extends StandardSchemaV1<unknown, Record<string, string>>,
  const Events extends Record<string, StandardSchemaV1>,
>(
  name: Name,
  options: { params: Params; events: Events },
): ChannelDefinition<Name, Params, Events> {
  if (!namePattern.test(name))
    throw new BroadcastValidationError("Invalid broadcast channel name");
  const events = Object.entries(options.events);
  if (!events.length || events.some(([event]) => !namePattern.test(event))) {
    throw new BroadcastValidationError(
      `Channel ${name} needs valid event names`,
    );
  }
  return Object.freeze({
    kind: "channel",
    name,
    params: options.params,
    events: Object.freeze({ ...options.events }),
  });
}

/** Validate both sides of a transport boundary; schema transforms must be stable. */
async function parseStable(
  schema: StandardSchemaV1,
  input: unknown,
  label: string,
): Promise<unknown> {
  try {
    const parsed = await schema["~standard"].validate(input);
    if (parsed.issues) throw new BroadcastValidationError(`Invalid ${label}`);
    const canonical = toEventTransportValue(label, parsed.value);
    const second = await schema["~standard"].validate(canonical);
    if (
      second.issues ||
      !eventTransportValuesEqual(
        canonical,
        toEventTransportValue(label, second.value),
      )
    ) {
      throw new BroadcastValidationError(
        `Unstable transport schema for ${label}`,
      );
    }
    return canonical;
  } catch (error) {
    if (error instanceof BroadcastValidationError) throw error;
    throw new BroadcastValidationError(`Invalid ${label}`);
  }
}

/** Provider and transport boundary validation. Uses parsed, canonical parameters. */
export async function parseChannelParams<C extends ChannelDefinition>(
  channel: C,
  input: unknown,
): Promise<InferChannelParams<C>> {
  const parsed = await parseStable(
    channel.params,
    input,
    `${channel.name} parameters`,
  );
  if (
    !parsed ||
    typeof parsed !== "object" ||
    Array.isArray(parsed) ||
    Object.values(parsed).some((value) => typeof value !== "string")
  ) {
    throw new BroadcastValidationError(
      `Channel ${channel.name} parameters must be a flat string record`,
    );
  }
  if (
    new TextEncoder().encode(JSON.stringify(parsed)).byteLength >
    broadcastLimits.requestBytes
  ) {
    throw new BroadcastValidationError(
      `Channel ${channel.name} parameters are too large`,
    );
  }
  return parsed as InferChannelParams<C>;
}

/** Provider and browser boundary validation. Event names select their schema. */
export async function parseChannelEvent<C extends ChannelDefinition>(
  channel: C,
  input: { event: string; data: unknown },
): Promise<InferChannelEvent<C>> {
  if (!Object.hasOwn(channel.events, input.event))
    throw new BroadcastValidationError(
      `Unknown event on channel ${channel.name}`,
    );
  const data = await parseStable(
    channel.events[input.event],
    input.data,
    `${channel.name}.${input.event}`,
  );
  if (
    new TextEncoder().encode(JSON.stringify(data)).byteLength >
    broadcastLimits.payloadBytes
  ) {
    throw new BroadcastValidationError(
      `Channel ${channel.name} payload is too large`,
    );
  }
  return { event: input.event, data } as InferChannelEvent<C>;
}

/** Collision-free identity independent of parameter property order. Parse parameters first. */
export function channelKey(
  channel: string,
  params: Readonly<Record<string, string>>,
): string {
  return JSON.stringify([
    channel,
    Object.entries(params).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),
  ]);
}
