import { BestAgentError, isRecord } from "@best-agent/foundation";
import type { ToolInputValue } from "@best-agent/protocol";
import { isDeepEqual, isPlainObject } from "./builtins/shared.js";
import type {
  RuntimeToolDefinition,
  ToolDefinition,
  ToolExecutionContext,
  ToolInputBase,
  ToolInputJsonSchema,
} from "./contracts.js";
import { inferToolEffectFromExecutionClass } from "./effect.js";
import { isZodToolInputSchema, parseZodToolInput, type ZodToolInputSchema } from "./schema.js";

type JsonSchema = ToolInputJsonSchema;

export function toRuntimeToolDefinition<TInput extends ToolInputBase>(
  tool: ToolDefinition<TInput>,
): RuntimeToolDefinition {
  const summarize = tool.summarize;
  return {
    approvalClass: tool.approvalClass,
    capabilities(input, context) {
      return tool.capabilities(input as TInput, context);
    },
    concurrencyGroup: tool.concurrencyGroup,
    description: tool.description,
    effect: tool.effect ?? inferToolEffectFromExecutionClass(tool.executionClass),
    evidenceExtractor: tool.evidenceExtractor
      ? (input, output, envelope) =>
          tool.evidenceExtractor?.(input as TInput, output, envelope) ?? []
      : undefined,
    execute(input, context) {
      return tool.execute(input as TInput, context);
    },
    executionClass: tool.executionClass,
    expectedDurationClass: tool.expectedDurationClass,
    formatInput(input) {
      return tool.formatInput?.(input as TInput) ?? formatToolInput(input);
    },
    heartbeatPolicy: tool.heartbeatPolicy,
    historyInput: tool.historyInput ? (input) => tool.historyInput?.(input as TInput) : undefined,
    idempotent: tool.idempotent,
    inputSchema: tool.inputSchema,
    isContentBearingInput: tool.isContentBearingInput
      ? (input) => tool.isContentBearingInput?.(input as TInput) ?? false
      : undefined,
    name: tool.name,
    async parse(input, context) {
      return toToolInputValue(await parseToolInputForDefinition(tool, input, context));
    },
    providerHistoryInput: tool.providerHistoryInput
      ? (input) => tool.providerHistoryInput?.(input as TInput) ?? input
      : undefined,
    replayPolicy: tool.replayPolicy,
    resultEnvelope: tool.resultEnvelope
      ? (input, output) => tool.resultEnvelope?.(input as TInput, output) ?? {}
      : undefined,
    source: tool.source,
    sourceExtension: tool.sourceExtension,
    sourcePlugin: tool.sourcePlugin,
    sourceServer: tool.sourceServer,
    summarize: summarize ? (input, output) => summarize(input as TInput, output) : undefined,
  };
}

export async function parseToolInputForDefinition<TInput extends ToolInputBase>(
  tool: ToolDefinition<TInput>,
  input: ToolInputValue,
  context: ToolExecutionContext,
): Promise<TInput> {
  const toolInputMode = context.toolInputMode ?? "strict";
  if (toolInputMode === "legacy" && tool.legacyAdapter) {
    const preparedInput = await tool.legacyAdapter.adapt(input, context);
    if (!isZodToolInputSchema(tool.inputSchema)) {
      return preparedInput as TInput;
    }
    return parseToolInput(
      tool.name,
      tool.inputSchema as ZodToolInputSchema<TInput>,
      preparedInput as ToolInputBase,
    );
  }
  return parseToolInput(tool.name, tool.inputSchema, input as ToolInputBase) as TInput;
}

export function parseToolInputForDefinitionSync<TInput extends ToolInputBase>(
  tool: ToolDefinition<TInput>,
  input: ToolInputValue,
): TInput {
  if (isZodToolInputSchema(tool.inputSchema)) {
    return parseToolInput(tool.name, tool.inputSchema as ZodToolInputSchema<TInput>, input);
  }
  return parseToolInput(tool.name, tool.inputSchema, input);
}

export function parseRuntimeToolInput(
  toolName: string,
  inputSchema: unknown,
  input: ToolInputValue,
): ToolInputValue {
  return toToolInputValue(parseToolInput(toolName, inputSchema, input));
}

function parseToolInput<TInput extends ToolInputBase>(
  toolName: string,
  inputSchema: ZodToolInputSchema<TInput>,
  input: ToolInputBase,
): TInput;
function parseToolInput<TInput extends ToolInputBase>(
  toolName: string,
  inputSchema: unknown,
  input: ToolInputBase,
): TInput;
function parseToolInput<TInput extends ToolInputBase>(
  toolName: string,
  inputSchema: unknown,
  input: ToolInputBase,
): TInput {
  if (isZodToolInputSchema(inputSchema)) {
    return parseZodToolInput(toolName, inputSchema as ZodToolInputSchema<TInput>, input);
  }
  if (!isSchemaObject(inputSchema)) {
    return input as TInput;
  }

  validateAgainstSchema(toolName, inputSchema, input, "input");
  return input as TInput;
}

function validateAgainstSchema(
  toolName: string,
  schema: JsonSchema,
  value: ToolInputBase,
  path: string,
): void {
  if (schema.enum && !schema.enum.some((candidate) => isDeepEqual(candidate, value))) {
    throw new BestAgentError(
      "TOOL_INPUT_INVALID",
      `${toolName} ${path} must match one of the allowed values.`,
    );
  }

  if (schema.type) {
    const types = Array.isArray(schema.type) ? schema.type : [schema.type];
    if (!types.some((type) => valueMatchesType(value, type))) {
      throw new BestAgentError(
        "TOOL_INPUT_INVALID",
        `${toolName} ${path} does not match the expected schema.`,
      );
    }
  }

  if (schema.type === "object") {
    if (!isPlainObject(value)) {
      return;
    }
    const properties = schema.properties ?? {};
    const required = new Set(schema.required ?? []);
    for (const key of required) {
      if (!(key in value)) {
        throw new BestAgentError("TOOL_INPUT_INVALID", `${toolName} ${path}.${key} is required.`);
      }
    }
    for (const [key, candidate] of Object.entries(value)) {
      const childSchema = properties[key];
      if (!childSchema) {
        if (schema.additionalProperties === false) {
          throw new BestAgentError(
            "TOOL_INPUT_INVALID",
            `${toolName} ${path}.${key} is not allowed.`,
          );
        }
        continue;
      }
      validateAgainstSchema(toolName, childSchema, candidate as ToolInputBase, `${path}.${key}`);
    }
  }

  if (schema.type === "array" && Array.isArray(value) && schema.items) {
    value.forEach((item, index) =>
      validateAgainstSchema(toolName, schema.items!, item as ToolInputBase, `${path}[${index}]`),
    );
  }
}

function valueMatchesType(
  value: ToolInputBase,
  type: NonNullable<JsonSchema["type"]>[number],
): boolean {
  switch (type) {
    case "array":
      return Array.isArray(value);
    case "boolean":
      return typeof value === "boolean";
    case "integer":
      return typeof value === "number" && Number.isInteger(value);
    case "number":
      return typeof value === "number";
    case "object":
      return isPlainObject(value);
    case "string":
      return typeof value === "string";
  }
  return false;
}

function isSchemaObject(value: unknown): value is JsonSchema {
  return isRecord(value);
}

function toToolInputValue(input: ToolInputBase): ToolInputValue {
  return input as ToolInputValue;
}

function formatToolInput(input: ToolInputValue): string {
  if (typeof input === "string") {
    return input;
  }
  return JSON.stringify(input);
}
