import {
  ICE_CONFIG_PROTOCOL_VERSION,
  type IceConfigGrant,
  type IceConfigRequest,
  type IceConfigResponse,
  type IceConfigValidationResult,
  type StunIceServer,
  type TurnIceServer,
} from "./iceTypes.ts";

export const MAX_ICE_URLS = 8;
export const MAX_ICE_CREDENTIAL_LIFETIME_MS = 3_600_000;

type JsonRecord = Record<string, unknown>;

export function isSafeIceUrl(value: unknown): value is string {
  if (typeof value !== "string" || value.length < 6 || value.length > 512) return false;
  if (/[\u0000-\u0020\u007f]/.test(value) || /[/#@]/.test(value)) return false;
  // TURN's one useful query parameter is the transport selector. Accept that
  // exact grammar so deployments can offer UDP, TCP, and TLS fallbacks without
  // opening a generic query-string channel for credentials or vendor options.
  const match = /^(stun|stuns|turn|turns):([^?]+)(?:\?transport=(udp|tcp))?$/.exec(value);
  if (!match) return false;
  const scheme = match[1] as string;
  const authority = match[2] as string;
  const transport = match[3];
  if (transport !== undefined && scheme !== "turn" && scheme !== "turns") return false;
  if (authority.startsWith("[") && /^\[[0-9a-fA-F:.]+\](?::[1-9][0-9]{0,4})?$/.test(authority)) {
    return validPort(authority);
  }
  if (!/^[a-zA-Z0-9.-]+(?::[1-9][0-9]{0,4})?$/.test(authority)) return false;
  const host = authority.replace(/:[0-9]+$/, "");
  return host.length <= 253 && !host.startsWith(".") && !host.endsWith(".") &&
    !host.includes("..") && validPort(authority);
}

export function parseIceConfigRequest(value: unknown): IceConfigValidationResult<IceConfigRequest> {
  if (!exact(value, ["type", "protocolVersion", "requestId", "binding", "credential"]) ||
      value.type !== "ice-config-request" || value.protocolVersion !== ICE_CONFIG_PROTOCOL_VERSION ||
      !identifier(value.requestId) || !screenBinding(value.binding) ||
      !screenCredential(value.credential)) return failure("invalid request");
  return success(value as unknown as IceConfigRequest);
}

function screenBinding(value: unknown): boolean {
  return exact(value, ["officeId", "levelId", "roomId", "screenId"]) &&
    identifier(value.officeId) && identifier(value.levelId) &&
    (value.roomId === null || identifier(value.roomId)) && identifier(value.screenId);
}

function screenCredential(value: unknown): boolean {
  return exact(value, ["sessionId", "participantId", "role", "grantToken"]) &&
    opaqueIdentifier(value.sessionId) && opaqueIdentifier(value.participantId) &&
    (value.role === "presenter" || value.role === "viewer") &&
    typeof value.grantToken === "string" && value.grantToken.length >= 16 &&
    value.grantToken.length <= 512;
}

/** Server credentials use unpadded base64url and may begin with `_` or `-`. */
function opaqueIdentifier(value: unknown): value is string {
  return typeof value === "string" && /^[a-zA-Z0-9_-]{1,256}$/.test(value);
}

export function parseIceConfigResponse(value: unknown): IceConfigValidationResult<IceConfigResponse> {
  if (!plain(value) || value.protocolVersion !== ICE_CONFIG_PROTOCOL_VERSION || !identifier(value.requestId)) {
    return failure("invalid response envelope");
  }
  if (value.type === "ice-config-unavailable") {
    if (!exact(value, ["type", "protocolVersion", "requestId", "retryAfterMs"]) ||
        !boundedInteger(value.retryAfterMs, 1_000, 3_600_000)) return failure("invalid unavailable response");
    return success(value as unknown as IceConfigResponse);
  }
  if (value.type !== "ice-config-grant" ||
      !exact(value, ["type", "protocolVersion", "requestId", "issuedAtMs", "expiresAtMs", "iceServers"]) ||
      !timestamp(value.issuedAtMs) || !timestamp(value.expiresAtMs) ||
      value.expiresAtMs <= value.issuedAtMs ||
      value.expiresAtMs - value.issuedAtMs > MAX_ICE_CREDENTIAL_LIFETIME_MS ||
      !Array.isArray(value.iceServers) || value.iceServers.length < 1 || value.iceServers.length > 2) {
    return failure("invalid grant");
  }
  const servers = value.iceServers;
  if (!servers.every(iceServer) || !servers.some(isTurnIceServer)) return failure("grant requires TURN");
  return success(value as unknown as IceConfigGrant);
}

export function isIceConfigGrantActive(value: IceConfigGrant, nowMs: number): boolean {
  return parseIceConfigResponse(value).ok && timestamp(nowMs) &&
    nowMs >= value.issuedAtMs && nowMs < value.expiresAtMs;
}

function iceServer(value: unknown): value is StunIceServer | TurnIceServer {
  if (!plain(value)) return false;
  if (exact(value, ["urls"])) return urls(value.urls, ["stun:", "stuns:"]);
  return isTurnIceServer(value);
}

function isTurnIceServer(value: unknown): value is TurnIceServer {
  return exact(value, ["urls", "username", "credential", "credentialType"]) &&
    urls(value.urls, ["turn:", "turns:"]) && opaque(value.username, 16, 256) &&
    opaque(value.credential, 20, 256) && value.credentialType === "password";
}

function urls(value: unknown, schemes: readonly string[]): value is readonly string[] {
  return Array.isArray(value) && value.length > 0 && value.length <= MAX_ICE_URLS &&
    value.every((url) => isSafeIceUrl(url) && schemes.some((scheme) => url.startsWith(scheme))) &&
    new Set(value).size === value.length;
}

function validPort(authority: string): boolean {
  const match = /:([0-9]+)$/.exec(authority);
  return match === null || Number(match[1]) <= 65_535;
}

function timestamp(value: unknown): value is number {
  return Number.isSafeInteger(value) && (value as number) >= 0 && (value as number) <= 8_640_000_000_000_000;
}

function boundedInteger(value: unknown, minimum: number, maximum: number): value is number {
  return Number.isSafeInteger(value) && (value as number) >= minimum && (value as number) <= maximum;
}

function identifier(value: unknown): value is string {
  return typeof value === "string" && /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/.test(value);
}

function opaque(value: unknown, minimum: number, maximum: number): value is string {
  return typeof value === "string" && value.length >= minimum && value.length <= maximum &&
    /^[a-zA-Z0-9_:+/=.-]+$/.test(value);
}

function plain(value: unknown): value is JsonRecord {
  return typeof value === "object" && value !== null && !Array.isArray(value) &&
    Object.getPrototypeOf(value) === Object.prototype;
}

function exact(value: unknown, keys: readonly string[]): value is JsonRecord {
  if (!plain(value)) return false;
  const actual = Object.keys(value);
  return actual.length === keys.length && actual.every((key) => keys.includes(key));
}

function success<T>(value: T): IceConfigValidationResult<T> { return { ok: true, value }; }
function failure<T>(error: string): IceConfigValidationResult<T> { return { ok: false, error: `ice config: ${error}` }; }
