#!/usr/bin/env bash
set -euo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"

node - "$ROOT" <<'NODE'
const fs = require("node:fs");
const path = require("node:path");

function fail(message) {
  console.error(message);
  process.exit(1);
}

function readJson(filePath) {
  try {
    return JSON.parse(fs.readFileSync(filePath, "utf8"));
  } catch (error) {
    fail(`failed to read ${filePath}: ${error.message}`);
  }
}

function expect(condition, message) {
  if (!condition) {
    fail(message);
  }
}

const root = process.argv[2];
const schemaPath = path.join(root, "spec", "adapters.schema.json");
const catalogPath = path.join(root, "presets", "adapters.json");

const schema = readJson(schemaPath);
const catalog = readJson(catalogPath);

expect(schema.additionalProperties === false, "schema must set top-level additionalProperties:false");
expect(Array.isArray(schema.required) && schema.required.includes("version") && schema.required.includes("adapters"), "schema must require version and adapters");
expect(schema.properties && schema.properties.version && schema.properties.version.const === "adapters/v1", "schema must pin version to adapters/v1");
expect(schema.properties && schema.properties.adapters && schema.properties.adapters.type === "array", "schema must define adapters as an array");

const adapterSchema = schema.$defs && schema.$defs.adapter;
expect(adapterSchema && adapterSchema.type === "object", "schema must define $defs.adapter");
expect(adapterSchema.additionalProperties === false, "adapter schema must set additionalProperties:false");
expect(Array.isArray(adapterSchema.required) && adapterSchema.required.includes("id") && adapterSchema.required.includes("wrapper") && adapterSchema.required.includes("safeProbeArgv"), "adapter schema must require id, wrapper, and safeProbeArgv");
expect(adapterSchema.properties && adapterSchema.properties.id && adapterSchema.properties.wrapper && adapterSchema.properties.safeProbeArgv, "adapter schema must define id, wrapper, and safeProbeArgv");
expect(!Object.prototype.hasOwnProperty.call(adapterSchema.properties, "exec"), "adapter schema must not expose arbitrary exec field");
if (Object.prototype.hasOwnProperty.call(adapterSchema.properties, "listModels")) {
  expect(adapterSchema.properties.listModels.type === "boolean", "listModels must be boolean when present");
}
if (Object.prototype.hasOwnProperty.call(adapterSchema.properties, "models")) {
  expect(adapterSchema.properties.models.type === "array", "models must be an array when present");
}
if (Object.prototype.hasOwnProperty.call(adapterSchema.properties, "metered")) {
  expect(adapterSchema.properties.metered.type === "boolean", "metered must be boolean when present");
}
if (Object.prototype.hasOwnProperty.call(adapterSchema.properties, "safeProbeArgv")) {
  expect(adapterSchema.properties.safeProbeArgv.type === "array", "safeProbeArgv must be an array when present");
}

expect(catalog.version === "adapters/v1", "catalog version must be adapters/v1");
expect(Array.isArray(catalog.adapters) && catalog.adapters.length > 0, "catalog must contain at least one adapter");

const ids = new Set();
const wrappers = new Set();
for (const adapter of catalog.adapters) {
  expect(adapter && typeof adapter === "object" && !Array.isArray(adapter), "each adapter must be an object");

  const keys = Object.keys(adapter);
  for (const key of keys) {
    expect(["id", "wrapper", "description", "listModels", "models", "metered", "safeProbeArgv"].includes(key), `adapter ${JSON.stringify(adapter)} has unknown field ${key}`);
  }

  expect(typeof adapter.id === "string" && /^[a-z0-9][a-z0-9-]*$/.test(adapter.id), "adapter id must be kebab-case");
  expect(typeof adapter.wrapper === "string" && adapter.wrapper.length > 0, `adapter ${adapter.id} must set wrapper`);
  expect(Array.isArray(adapter.models) && adapter.models.length > 0 && adapter.models.every((m) => typeof m === "string" && m.length > 0), `adapter ${adapter.id} must define a non-empty string models array`);
  expect(Array.isArray(adapter.safeProbeArgv) && adapter.safeProbeArgv.length > 0 && adapter.safeProbeArgv.every((arg) => typeof arg === "string" && arg.length > 0), `adapter ${adapter.id} must define a non-empty safeProbeArgv string array`);
  expect(!ids.has(adapter.id), `duplicate adapter id: ${adapter.id}`);
  expect(!wrappers.has(adapter.wrapper), `duplicate wrapper path: ${adapter.wrapper}`);
  ids.add(adapter.id);
  wrappers.add(adapter.wrapper);

  if (Object.prototype.hasOwnProperty.call(adapter, "description")) {
    expect(typeof adapter.description === "string", `adapter ${adapter.id} description must be a string`);
  }
  if (Object.prototype.hasOwnProperty.call(adapter, "listModels")) {
    expect(typeof adapter.listModels === "boolean", `adapter ${adapter.id} listModels must be boolean`);
  }
  if (Object.prototype.hasOwnProperty.call(adapter, "models")) {
    expect(Array.isArray(adapter.models), `adapter ${adapter.id} models must be an array`);
    expect(adapter.models.every((model) => typeof model === "string" && model.length > 0), `adapter ${adapter.id} models must contain only non-empty strings`);
  }
  if (Object.prototype.hasOwnProperty.call(adapter, "metered")) {
    expect(typeof adapter.metered === "boolean", `adapter ${adapter.id} metered must be boolean`);
  }
  if (Object.prototype.hasOwnProperty.call(adapter, "safeProbeArgv")) {
    expect(Array.isArray(adapter.safeProbeArgv), `adapter ${adapter.id} safeProbeArgv must be an array`);
    expect(adapter.safeProbeArgv.every((arg) => typeof arg === "string" && arg.length > 0), `adapter ${adapter.id} safeProbeArgv must contain only non-empty strings`);
  }

  expect(!Object.prototype.hasOwnProperty.call(adapter, "exec"), `adapter ${adapter.id} must not expose arbitrary exec field`);
  expect(fs.existsSync(path.join(root, adapter.wrapper)), `adapter ${adapter.id} wrapper does not exist: ${adapter.wrapper}`);
}

console.log(`validated ${catalog.adapters.length} adapter(s)`);
NODE
