import {
  DEVICE_PLUGIN_API_VERSION_V1ALPHA1,
  DEVICE_PLUGIN_API_VERSION_V1ALPHA2,
  type DeviceCapability,
  type DeviceCompatibilityProfileDefinition,
  type DeviceModelDefinition,
  type DevicePluginActionDefinition,
  type DevicePluginManifest,
  type DevicePluginManifestV1Alpha1,
  type DevicePluginManifestV1Alpha2,
} from "./contracts";

function record(value: unknown, path: string): Record<string, unknown> {
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
    throw new Error(`Некорректный manifest: ${path} должен быть объектом.`);
  }
  return value as Record<string, unknown>;
}

function exactKeys(
  value: Record<string, unknown>,
  path: string,
  allowed: readonly string[],
): void {
  const extras = Object.keys(value).filter((key) => !allowed.includes(key));
  if (extras.length) {
    throw new Error(`Некорректный manifest: ${path} содержит неизвестные поля ${extras.join(", ")}.`);
  }
}

function text(value: unknown, path: string, maxLength = 160): string {
  if (typeof value !== "string" || !value.trim()) {
    throw new Error(`Некорректный manifest: ${path} должен быть непустой строкой.`);
  }
  if (value.length > maxLength) {
    throw new Error(
      `Некорректный manifest: ${path} длиннее ${maxLength} символов.`,
    );
  }
  return value;
}

function identifier(value: unknown, path: string): string {
  const candidate = text(value, path, 192);
  if (!/^[A-Za-z0-9][A-Za-z0-9._:/-]*$/.test(candidate)) {
    throw new Error(`Некорректный manifest: ${path} не является идентификатором.`);
  }
  return candidate;
}

function flag(value: unknown, path: string): boolean {
  if (typeof value !== "boolean") {
    throw new Error(`Некорректный manifest: ${path} должен быть boolean.`);
  }
  return value;
}

function list(value: unknown, path: string): unknown[] {
  if (!Array.isArray(value)) {
    throw new Error(`Некорректный manifest: ${path} должен быть массивом.`);
  }
  return value;
}

function contractText(value: unknown, path: string, strictIdentifiers: boolean): string {
  return strictIdentifiers ? identifier(value, path) : text(value, path);
}

function capability(
  value: unknown,
  path: string,
  strictIdentifiers: boolean,
): DeviceCapability {
  const item = record(value, path);
  exactKeys(item, path, ["id", "label"]);
  return {
    id: contractText(item.id, `${path}.id`, strictIdentifiers),
    label: text(item.label, `${path}.label`),
  };
}

function action(
  value: unknown,
  path: string,
  strictIdentifiers: boolean,
): DevicePluginActionDefinition {
  const item = record(value, path);
  exactKeys(item, path, ["id", "mutating", "secretFields"]);
  return {
    id: contractText(item.id, `${path}.id`, strictIdentifiers),
    mutating: flag(item.mutating, `${path}.mutating`),
    secretFields: list(item.secretFields, `${path}.secretFields`).map((field, index) =>
      contractText(field, `${path}.secretFields[${index}]`, strictIdentifiers),
    ),
  };
}

function model(
  value: unknown,
  path: string,
  strictIdentifiers: boolean,
): DeviceModelDefinition {
  const item = record(value, path);
  exactKeys(item, path, [
    "id",
    "vendor",
    "displayName",
    "category",
    "description",
    "verified",
    "capabilities",
    "ui",
  ]);
  const ui = record(item.ui, `${path}.ui`);
  exactKeys(ui, `${path}.ui`, ["slot", "componentKey"]);
  const slot = text(ui.slot, `${path}.ui.slot`);
  if (slot !== "device.connection") {
    throw new Error(`Некорректный manifest: слот ${slot} пока не поддерживается.`);
  }
  return {
    id: contractText(item.id, `${path}.id`, strictIdentifiers),
    vendor: text(item.vendor, `${path}.vendor`),
    displayName: text(item.displayName, `${path}.displayName`),
    category: text(item.category, `${path}.category`),
    description: text(item.description, `${path}.description`, 1024),
    verified: flag(item.verified, `${path}.verified`),
    capabilities: list(item.capabilities, `${path}.capabilities`).map((entry, index) =>
      capability(entry, `${path}.capabilities[${index}]`, strictIdentifiers),
    ),
    ui: {
      slot,
      componentKey: text(ui.componentKey, `${path}.ui.componentKey`),
    },
  };
}

function compatibilityProfile(
  value: unknown,
  path: string,
): DeviceCompatibilityProfileDefinition {
  const item = record(value, path);
  exactKeys(item, path, ["profileId", "path", "modelId"]);
  const profilePath = text(item.path, `${path}.path`, 512);
  const pathSegments = profilePath.split("/");
  if (
    profilePath.startsWith("/") ||
    profilePath.includes("\\") ||
    !profilePath.endsWith(".json") ||
    pathSegments.some((segment) => !segment || segment === "." || segment === "..")
  ) {
    throw new Error(
      `Некорректный manifest: ${path}.path должен быть безопасным относительным JSON-путём.`,
    );
  }
  return {
    profileId: identifier(item.profileId, `${path}.profileId`),
    path: profilePath,
    modelId: identifier(item.modelId, `${path}.modelId`),
  };
}

function validateV1Alpha2Profiles(
  models: readonly DeviceModelDefinition[],
  profiles: readonly DeviceCompatibilityProfileDefinition[],
): void {
  if (!profiles.length) {
    throw new Error("Manifest v1alpha2 должен объявлять compatibilityProfiles.");
  }
  const modelIds = new Set(models.map((modelItem) => modelItem.id));
  const profileIds = new Set<string>();
  const profilePaths = new Set<string>();
  const coveredModels = new Set<string>();
  for (const profile of profiles) {
    if (profileIds.has(profile.profileId) || profilePaths.has(profile.path)) {
      throw new Error(
        `Manifest v1alpha2 повторяет профиль ${profile.profileId} или его путь.`,
      );
    }
    profileIds.add(profile.profileId);
    profilePaths.add(profile.path);
    if (!modelIds.has(profile.modelId)) {
      throw new Error(
        `Профиль ${profile.profileId} ссылается на неизвестную модель ${profile.modelId}.`,
      );
    }
    coveredModels.add(profile.modelId);
  }
  const uncovered = [...modelIds].filter((modelId) => !coveredModels.has(modelId));
  if (uncovered.length) {
    throw new Error(
      `Manifest v1alpha2 не содержит разрешённого профиля для моделей: ${uncovered.join(", ")}.`,
    );
  }
}

export function parseDevicePluginManifest(document: unknown): DevicePluginManifest {
  const root = record(document, "root");
  exactKeys(root, "root", ["apiVersion", "kind", "metadata", "spec"]);
  const apiVersion = root.apiVersion;
  if (
    (apiVersion !== DEVICE_PLUGIN_API_VERSION_V1ALPHA1 &&
      apiVersion !== DEVICE_PLUGIN_API_VERSION_V1ALPHA2) ||
    root.kind !== "DevicePlugin"
  ) {
    throw new Error("Manifest использует несовместимую версию или kind.");
  }
  const metadata = record(root.metadata, "metadata");
  exactKeys(metadata, "metadata", ["id", "version", "displayName"]);
  const spec = record(root.spec, "spec");
  const specKeys = [
    "hostApiRange",
    "runtime",
    "permissions",
    "actions",
    "models",
  ];
  exactKeys(
    spec,
    "spec",
    apiVersion === DEVICE_PLUGIN_API_VERSION_V1ALPHA2
      ? [...specKeys, "compatibilityProfiles"]
      : specKeys,
  );
  const expectedHostApiRange =
    apiVersion === DEVICE_PLUGIN_API_VERSION_V1ALPHA2 ? "v1alpha2" : "v1alpha1";
  if (spec.hostApiRange !== expectedHostApiRange) {
    throw new Error(`Manifest требует несовместимый host API: ${String(spec.hostApiRange)}.`);
  }
  const runtime = record(spec.runtime, "spec.runtime");
  exactKeys(runtime, "spec.runtime", ["backendEntrypoint", "isolation"]);
  const isolation = text(runtime.isolation, "spec.runtime.isolation");
  if (isolation !== "transitional-in-process") {
    throw new Error(`Некорректный manifest: неизвестная изоляция ${isolation}.`);
  }

  const strictIdentifiers = apiVersion === DEVICE_PLUGIN_API_VERSION_V1ALPHA2;
  const models = list(spec.models, "spec.models").map((entry, index) =>
    model(entry, `spec.models[${index}]`, strictIdentifiers),
  );
  if (apiVersion === DEVICE_PLUGIN_API_VERSION_V1ALPHA1 && models.length !== 1) {
    throw new Error("Manifest v1alpha1 должен объявлять ровно одну модель.");
  }
  if (apiVersion === DEVICE_PLUGIN_API_VERSION_V1ALPHA2 && !models.length) {
    throw new Error("Manifest v1alpha2 должен объявлять хотя бы одну модель.");
  }

  const version = text(metadata.version, "metadata.version");
  if (!/^[0-9]+\.[0-9]+\.[0-9]+(?:[-+][A-Za-z0-9.-]+)?$/.test(version)) {
    throw new Error(`Некорректный manifest: версия ${version} не является semver.`);
  }

  const common = {
    kind: "DevicePlugin",
    metadata: {
      id: contractText(metadata.id, "metadata.id", strictIdentifiers),
      version,
      displayName: text(metadata.displayName, "metadata.displayName"),
    },
  } as const;
  const commonSpec = {
    runtime: {
      backendEntrypoint: text(
        runtime.backendEntrypoint,
        "spec.runtime.backendEntrypoint",
        256,
      ),
      isolation,
    },
    permissions: list(spec.permissions, "spec.permissions").map((entry, index) =>
      contractText(entry, `spec.permissions[${index}]`, strictIdentifiers),
    ),
    actions: list(spec.actions, "spec.actions").map((entry, index) =>
      action(entry, `spec.actions[${index}]`, strictIdentifiers),
    ),
    models,
  } as const;

  if (apiVersion === DEVICE_PLUGIN_API_VERSION_V1ALPHA1) {
    return {
      ...common,
      apiVersion,
      spec: {
        ...commonSpec,
        hostApiRange: "v1alpha1",
      },
    } satisfies DevicePluginManifestV1Alpha1;
  }

  const profiles = list(spec.compatibilityProfiles, "spec.compatibilityProfiles").map(
    (entry, index) => compatibilityProfile(entry, `spec.compatibilityProfiles[${index}]`),
  );
  validateV1Alpha2Profiles(models, profiles);
  return {
    ...common,
    apiVersion,
    spec: {
      ...commonSpec,
      hostApiRange: "v1alpha2",
      compatibilityProfiles: profiles,
    },
  } satisfies DevicePluginManifestV1Alpha2;
}

export function requirePluginAction(
  manifest: DevicePluginManifest,
  actionId: string,
): string {
  const action = manifest.spec.actions.find((candidate) => candidate.id === actionId);
  if (!action) {
    throw new Error(`Плагин ${manifest.metadata.id} не объявляет действие ${actionId}.`);
  }
  return action.id;
}
