import type {
  ToolOptionField,
  ToolOptionValue,
  ToolOptionsSchema,
  ToolOptionsValue,
} from "./plugin-types";
import type {
  ToolInput,
  ToolInputDefinition,
  ToolInputFieldDefinition,
} from "../io/input-types";
import { getInputValue } from "../io/input-types";

function describeInput(field: ToolInputFieldDefinition): string {
  return field.label ?? field.key ?? "input";
}

function matchesAccept(file: File, accept: string[]): boolean {
  if (accept.length === 0) return true;

  return accept.some((rule) => {
    if (rule.endsWith("/*")) {
      return file.type.startsWith(rule.slice(0, -1));
    }

    if (rule.startsWith(".")) {
      return file.name.toLowerCase().endsWith(rule.toLowerCase());
    }

    return file.type === rule;
  });
}

function validateText(field: Extract<ToolInputFieldDefinition, { type: "text" }>, text?: string) {
  if (field.maxLength !== undefined && (text?.length ?? 0) > field.maxLength) {
    throw new Error(`${describeInput(field)} must be ${field.maxLength} characters or fewer.`);
  }
}

function validateFiles(
  field: Extract<ToolInputFieldDefinition, { type: "files" | "text-or-files" }>,
  files?: File[]
) {
  if (!files || files.length === 0) return;

  if ("multiple" in field && field.multiple === false && files.length > 1) {
    throw new Error(`${describeInput(field)} accepts only one file.`);
  }

  if ("maxFiles" in field && field.maxFiles !== undefined && files.length > field.maxFiles) {
    throw new Error(`${describeInput(field)} accepts at most ${field.maxFiles} files.`);
  }

  if (field.accept) {
    const invalidFile = files.find((file) => !matchesAccept(file, field.accept ?? []));
    if (invalidFile) {
      throw new Error(`${invalidFile.name} is not an accepted file type.`);
    }
  }

  if (field.maxSizeMb !== undefined) {
    const maxBytes = field.maxSizeMb * 1024 * 1024;
    const oversized = files.find((file) => file.size > maxBytes);
    if (oversized) {
      throw new Error(`${oversized.name} is larger than ${field.maxSizeMb} MB.`);
    }
  }
}

function validateField(field: ToolInputFieldDefinition, input: ToolInput) {
  const value = getInputValue(input, field.key ?? "input");

  if (field.type === "text") {
    validateText(field, value.text);
    return;
  }

  if (field.type === "files") {
    validateFiles(field, value.files);
    return;
  }

  validateFiles(field, value.files);
}

export function validateToolInput(definition: ToolInputDefinition, input: ToolInput) {
  if (definition.type === "none") return;

  const fields = definition.type === "group" ? definition.fields : [definition];
  fields.forEach((field) => validateField(field, input));
}

function normalizeOptionField(
  field: ToolOptionField,
  rawValue: unknown
): ToolOptionValue {
  const value = rawValue ?? field.defaultValue;

  if (field.type === "text") {
    return typeof value === "string" ? value : String(value ?? "");
  }

  if (field.type === "boolean") {
    if (typeof value !== "boolean") {
      throw new Error(`${field.label} must be true or false.`);
    }
    return value;
  }

  if (field.type === "select") {
    if (typeof value !== "string") {
      throw new Error(`${field.label} must be one of the available options.`);
    }

    if (!field.options.some((option) => option.value === value)) {
      throw new Error(`${field.label} has an invalid option.`);
    }

    return value;
  }

  if (typeof value !== "number" || Number.isNaN(value)) {
    throw new Error(`${field.label} must be a number.`);
  }

  if (field.min !== undefined && value < field.min) {
    throw new Error(`${field.label} must be at least ${field.min}.`);
  }

  if (field.max !== undefined && value > field.max) {
    throw new Error(`${field.label} must be at most ${field.max}.`);
  }

  return value;
}

export function normalizeToolOptions(
  schema: ToolOptionsSchema | undefined,
  options: unknown
): ToolOptionsValue {
  if (!schema) return {};

  const raw = options && typeof options === "object"
    ? (options as Record<string, unknown>)
    : {};

  return schema.fields.reduce<ToolOptionsValue>((acc, field) => {
    acc[field.key] = normalizeOptionField(field, raw[field.key]);
    return acc;
  }, {});
}
