type JsonRecord = Record<string, unknown>;

export const COMBO_SCHEMA_VERSION = 2;

export interface ComboModelStep {
  id: string;
  kind: "model";
  model: string;
  providerId?: string | null;
  connectionId?: string | null;
  weight: number;
  label?: string;
  tags?: string[];
}

export interface ComboRefStep {
  id: string;
  kind: "combo-ref";
  comboName: string;
  weight: number;
  label?: string;
}

export type ComboStep = ComboModelStep | ComboRefStep;

type ComboCollectionLike =
  | Array<{ name?: unknown } | string>
  | { combos?: Array<{ name?: unknown }> }
  | Iterable<string>
  | null
  | undefined;

interface NormalizeComboStepOptions {
  comboName?: string | null;
  index?: number;
  allCombos?: ComboCollectionLike;
}

interface NormalizeComboRecordOptions {
  allCombos?: ComboCollectionLike;
}

function isRecord(value: unknown): value is JsonRecord {
  return !!value && typeof value === "object" && !Array.isArray(value);
}

function toTrimmedString(value: unknown): string | null {
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
}

function toWeight(value: unknown): number {
  const parsed =
    typeof value === "number"
      ? value
      : typeof value === "string" && value.trim().length > 0
        ? Number(value)
        : 0;

  if (!Number.isFinite(parsed)) return 0;
  return Math.max(0, Math.min(100, parsed));
}

function collectComboNames(allCombos: ComboCollectionLike): Set<string> {
  const names = new Set<string>();
  if (!allCombos) return names;

  if (
    !Array.isArray(allCombos) &&
    typeof (allCombos as { [Symbol.iterator]?: unknown })[Symbol.iterator] === "function" &&
    !(isRecord(allCombos) && Array.isArray(allCombos.combos))
  ) {
    for (const value of allCombos as Iterable<string>) {
      const name = toTrimmedString(value);
      if (name) names.add(name);
    }
    return names;
  }

  const combosFromRecord =
    isRecord(allCombos) && Array.isArray(allCombos.combos) ? allCombos.combos : [];
  const combos = Array.isArray(allCombos) ? allCombos : combosFromRecord;

  for (const combo of combos) {
    const name = typeof combo === "string" ? toTrimmedString(combo) : toTrimmedString(combo?.name);
    if (name) names.add(name);
  }

  return names;
}

function parseProviderId(model: string): string | null {
  const trimmed = model.trim();
  const slashIndex = trimmed.indexOf("/");
  if (slashIndex <= 0) return null;
  const providerId = trimmed.slice(0, slashIndex).trim();
  return providerId.length > 0 ? providerId : null;
}

function toFullModelString(model: string, providerId?: string | null): string {
  const trimmedModel = model.trim();
  if (trimmedModel.includes("/")) return trimmedModel;
  const normalizedProviderId = toTrimmedString(providerId);
  return normalizedProviderId ? `${normalizedProviderId}/${trimmedModel}` : trimmedModel;
}

function slugify(value: string): string {
  const slug = value
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-+|-+$/g, "");
  return slug.length > 0 ? slug : "step";
}

function buildStepId(
  kind: ComboStep["kind"],
  comboName: string | null,
  index: number,
  seed: string
) {
  const parts = [
    slugify(comboName || "combo"),
    kind === "combo-ref" ? "ref" : "model",
    String(index + 1),
    slugify(seed),
  ];
  return parts.join("-").slice(0, 200);
}

function shouldTreatAsComboRef(
  target: string,
  providerId: string | null,
  options: NormalizeComboStepOptions
): boolean {
  if (providerId) return false;
  const comboNames = collectComboNames(options.allCombos);
  return comboNames.has(target) || target === toTrimmedString(options.comboName);
}

export function isComboRefStep(value: unknown): value is ComboRefStep {
  return isRecord(value) && value.kind === "combo-ref" && !!toTrimmedString(value.comboName);
}

export function isComboModelStep(value: unknown): value is ComboModelStep {
  return isRecord(value) && value.kind === "model" && !!toTrimmedString(value.model);
}

export function getComboStepWeight(value: unknown): number {
  if (typeof value === "string") return 0;
  if (!isRecord(value)) return 0;
  return toWeight(value.weight);
}

export function getComboModelString(value: unknown): string | null {
  if (typeof value === "string") return toTrimmedString(value);
  if (!isRecord(value) || value.kind === "combo-ref") return null;

  const rawModel = toTrimmedString(value.model);
  if (!rawModel) return null;

  const providerId =
    toTrimmedString(value.providerId) ||
    toTrimmedString(value.provider) ||
    parseProviderId(rawModel);

  return toFullModelString(rawModel, providerId);
}

export function getComboModelProvider(value: unknown): string | null {
  if (typeof value === "string") return parseProviderId(value);
  if (!isRecord(value) || value.kind === "combo-ref") return null;

  return (
    toTrimmedString(value.providerId) ||
    toTrimmedString(value.provider) ||
    parseProviderId(toTrimmedString(value.model) || "")
  );
}

export function getComboStepTarget(
  value: unknown,
  options: NormalizeComboStepOptions = {}
): string | null {
  if (typeof value === "string") {
    const target = toTrimmedString(value);
    if (!target) return null;
    return shouldTreatAsComboRef(target, null, options) ? target : target;
  }

  if (!isRecord(value)) return null;
  if (value.kind === "combo-ref") return toTrimmedString(value.comboName);

  const rawModel = toTrimmedString(value.model);
  if (!rawModel) return null;
  const isExplicitModel = value.kind === "model";
  const providerId =
    toTrimmedString(value.providerId) ||
    toTrimmedString(value.provider) ||
    parseProviderId(rawModel);

  if (!isExplicitModel && shouldTreatAsComboRef(rawModel, providerId, options)) {
    return rawModel;
  }

  return toFullModelString(rawModel, providerId);
}

export function normalizeComboStep(
  value: unknown,
  options: NormalizeComboStepOptions = {}
): ComboStep | null {
  const comboName = toTrimmedString(options.comboName);
  const index = typeof options.index === "number" ? options.index : 0;

  if (typeof value === "string") {
    const target = toTrimmedString(value);
    if (!target) return null;

    if (shouldTreatAsComboRef(target, null, options)) {
      return {
        id: buildStepId("combo-ref", comboName, index, target),
        kind: "combo-ref",
        comboName: target,
        weight: 0,
      };
    }

    const providerId = parseProviderId(target);
    return {
      id: buildStepId("model", comboName, index, target),
      kind: "model",
      model: target,
      ...(providerId ? { providerId } : {}),
      weight: 0,
    };
  }

  if (!isRecord(value)) return null;

  const explicitId = toTrimmedString(value.id);
  const weight = toWeight(value.weight);
  const label = toTrimmedString(value.label);

  if (value.kind === "combo-ref") {
    const comboRefName = toTrimmedString(value.comboName);
    if (!comboRefName) return null;
    return {
      id: explicitId || buildStepId("combo-ref", comboName, index, comboRefName),
      kind: "combo-ref",
      comboName: comboRefName,
      weight,
      ...(label ? { label } : {}),
    };
  }

  const rawModel = toTrimmedString(value.model);
  if (!rawModel) return null;
  const isExplicitModel = value.kind === "model";

  const providerId =
    toTrimmedString(value.providerId) ||
    toTrimmedString(value.provider) ||
    parseProviderId(rawModel);

  if (!isExplicitModel && shouldTreatAsComboRef(rawModel, providerId, options)) {
    return {
      id: explicitId || buildStepId("combo-ref", comboName, index, rawModel),
      kind: "combo-ref",
      comboName: rawModel,
      weight,
      ...(label ? { label } : {}),
    };
  }

  const model = toFullModelString(rawModel, providerId);
  const connectionId =
    value.connectionId === null ? null : toTrimmedString(value.connectionId) || undefined;
  const tags = Array.isArray(value.tags)
    ? value.tags.map((tag) => toTrimmedString(tag)).filter((tag): tag is string => !!tag)
    : undefined;

  return {
    id:
      explicitId ||
      buildStepId("model", comboName, index, connectionId ? `${model}:${connectionId}` : model),
    kind: "model",
    model,
    ...(providerId ? { providerId } : {}),
    ...(connectionId !== undefined ? { connectionId } : {}),
    weight,
    ...(label ? { label } : {}),
    ...(tags && tags.length > 0 ? { tags } : {}),
  };
}

export function normalizeComboModels(
  models: unknown,
  options: Omit<NormalizeComboStepOptions, "index"> = {}
): ComboStep[] {
  const list = Array.isArray(models) ? models : [];
  return list
    .map((value, index) => normalizeComboStep(value, { ...options, index }))
    .filter((value): value is ComboStep => value !== null);
}

export function normalizeComboRecord<T extends JsonRecord>(
  combo: T,
  options: NormalizeComboRecordOptions = {}
): T & { version: 2; models: ComboStep[] } {
  const comboName = toTrimmedString(combo.name);
  return {
    ...combo,
    version: COMBO_SCHEMA_VERSION,
    models: normalizeComboModels(combo.models, {
      comboName,
      allCombos: options.allCombos,
    }),
  };
}
