import { z } from "zod";

// Stable v1 error-code enum (see spec "Error model").
export const ERROR_CODES = [
  "unauthorized",
  "not_authenticated",
  "unknown_type",
  "not_connected",
  "internal",
  "timeout",
] as const;
export type ErrorCode = (typeof ERROR_CODES)[number];

export type GatewayErrorBody = { code: string; message: string };

// A coded error. registry.request() rejects with this; the connection handler is
// the single place that turns a caught GatewayError back into an outbound error envelope.
export class GatewayError extends Error {
  readonly code: ErrorCode;

  constructor(code: ErrorCode, message?: string) {
    super(message ?? code);
    this.name = "GatewayError";
    this.code = code;
  }

  toBody(): GatewayErrorBody {
    return { code: this.code, message: this.message };
  }
}

// Empty-payload gotcha: CraftOS serializes `{}` as `"[]"`. Treat a missing / array /
// non-object payload as `{}` so downstream always sees a plain object.
const payloadSchema = z.preprocess(
  (value) =>
    typeof value === "object" && value !== null && !Array.isArray(value) ? value : {},
  z.record(z.string(), z.unknown()),
);

// Inbound errors are validated loosely: code may be any string (we only narrow our own).
const errorSchema = z.object({
  code: z.string(),
  message: z.string().optional(),
});

const computerRequestSchema = z.object({
  event: z.literal("computer_request"),
  traposSenderId: z.string().min(1),
  messageId: z.string().min(1),
  type: z.string().min(1),
  payload: payloadSchema,
});

const computerResponseSchema = z.object({
  event: z.literal("computer_response"),
  traposSenderId: z.string().min(1),
  messageId: z.string().min(1),
  type: z.string().min(1),
  ok: z.boolean(),
  payload: payloadSchema,
  error: errorSchema.optional(),
});

const gatewayRequestSchema = z.object({
  event: z.literal("server_request"),
  traposReceiverId: z.string().min(1),
  messageId: z.string().min(1),
  type: z.string().min(1),
  payload: payloadSchema,
});

const gatewayResponseSchema = z.object({
  event: z.literal("server_response"),
  traposReceiverId: z.string().min(1),
  messageId: z.string().min(1),
  type: z.string().min(1),
  ok: z.boolean(),
  payload: payloadSchema,
  error: errorSchema.optional(),
});

const frameSchema = z.discriminatedUnion("event", [
  computerRequestSchema,
  computerResponseSchema,
  gatewayRequestSchema,
  gatewayResponseSchema,
]);

export type Frame = z.infer<typeof frameSchema>;
export type ComputerRequest = z.infer<typeof computerRequestSchema>;
export type ComputerResponse = z.infer<typeof computerResponseSchema>;

// Parse + validate a raw frame. Throws on malformed JSON, schema failure, or an
// `event` outside the 4-event enum — the caller's try/catch drops (and logs) it.
// Does NOT validate `type`: an unknown type is a protocol *reply* (`unknown_type`),
// decided in dispatch, not a dropped frame.
export function parse(raw: string): Frame {
  const json: unknown = JSON.parse(raw);
  const result = frameSchema.safeParse(json);
  if (!result.success) {
    throw new Error(`invalid frame: ${result.error.message}`);
  }
  return result.data;
}

// Payload schema for the `hello` request. Strict: traposId must be a non-empty
// string; secret is optional (absent secret with auth on is treated as unauthorized).
export const helloPayloadSchema = z.object({
  traposId: z.string().min(1),
  secret: z.string().optional(),
});
