// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
//
// SPDX-License-Identifier: GPL-2.0-or-later

import type {
  DecoderDescriptor,
  RigListResponse,
  RigSnapshot,
} from "./generated";

export class ApiError extends Error {
  public constructor(
    public readonly status: number,
    message: string,
  ) {
    super(message);
    this.name = "ApiError";
  }
}

type Validator<T> = (value: unknown) => value is T;

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

export function isRigSnapshot(value: unknown): value is RigSnapshot {
  if (!isRecord(value) || !isRecord(value.info) || !isRecord(value.status)) {
    return false;
  }
  const { info, status } = value;
  return (
    typeof value.initialized === "boolean" &&
    typeof info.manufacturer === "string" &&
    typeof info.model === "string" &&
    isRecord(status.freq) &&
    typeof status.freq.hz === "number" &&
    (typeof status.mode === "string" || isRecord(status.mode)) &&
    typeof status.tx_en === "boolean"
  );
}

export function isRigListResponse(value: unknown): value is RigListResponse {
  return (
    isRecord(value) &&
    (value.active_remote === null || typeof value.active_remote === "string") &&
    Array.isArray(value.rigs) &&
    value.rigs.every(
      (rig) =>
        isRecord(rig) &&
        typeof rig.remote === "string" &&
        typeof rig.manufacturer === "string" &&
        typeof rig.model === "string" &&
        Array.isArray(rig.supported_modes) &&
        typeof rig.tx === "boolean" &&
        typeof rig.filter_controls === "boolean" &&
        typeof rig.initialized === "boolean",
    )
  );
}

export function isDecoderRegistry(value: unknown): value is DecoderDescriptor[] {
  return (
    Array.isArray(value) &&
    value.every(
      (decoder) =>
        isRecord(decoder) &&
        typeof decoder.id === "string" &&
        typeof decoder.label === "string" &&
        (decoder.activation === "mode_bound" || decoder.activation === "toggle") &&
        Array.isArray(decoder.active_modes) &&
        decoder.active_modes.every((mode) => typeof mode === "string") &&
        typeof decoder.background_decode === "boolean" &&
        typeof decoder.bookmark_selectable === "boolean",
    )
  );
}

export class TrxApi {
  public constructor(private readonly baseUrl = "") {}

  public async get<T>(path: string, validate: Validator<T>): Promise<T> {
    return this.request(path, { cache: "no-store" }, validate);
  }

  public async post<T>(
    path: string,
    body: unknown,
    validate: Validator<T>,
  ): Promise<T> {
    return this.request(
      path,
      {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
      },
      validate,
    );
  }

  private async request<T>(
    path: string,
    init: RequestInit,
    validate: Validator<T>,
  ): Promise<T> {
    const response = await fetch(`${this.baseUrl}${path}`, init);
    if (!response.ok) {
      const detail = await response.text();
      throw new ApiError(response.status, detail || response.statusText);
    }
    const value: unknown = await response.json();
    if (!validate(value)) {
      throw new ApiError(response.status, `Malformed response from ${path}`);
    }
    return value;
  }
}

export function decodeServerEvent<T>(
  event: MessageEvent<string>,
  validate: Validator<T>,
): T {
  let value: unknown;
  try {
    value = JSON.parse(event.data) as unknown;
  } catch (error: unknown) {
    throw new TypeError("Server event is not valid JSON", { cause: error });
  }
  if (!validate(value)) {
    throw new TypeError("Server event has an unexpected shape");
  }
  return value;
}
