// file: libs/plugins/src/codecall/codecall.types.ts

import { z } from '@frontmcp/lazy-zod';

// ===== Filter Function Types =====

/**
 * Tool info passed to the directCalls filter function
 */
export interface DirectCallsFilterToolInfo {
  name: string;
  appId?: string;
  source?: string;
  tags?: string[];
}

/**
 * Tool info passed to the includeTools filter function
 */
export interface IncludeToolsFilterToolInfo {
  name: string;
  appId?: string;
  source?: string;
  description?: string;
  tags?: string[];
}

/**
 * Function type for directCalls filter
 */
export type DirectCallsFilterFn = (tool: DirectCallsFilterToolInfo) => boolean;

/**
 * Function type for includeTools filter
 */
export type IncludeToolsFilterFn = (tool: IncludeToolsFilterToolInfo) => boolean;

// Helper schemas for filter functions with runtime validation
const directCallsFilterSchema = z.custom<DirectCallsFilterFn>((val) => typeof val === 'function', {
  message: 'filter must be a function with signature (tool: DirectCallsFilterToolInfo) => boolean',
});

const includeToolsFilterSchema = z.custom<IncludeToolsFilterFn>((val) => typeof val === 'function', {
  message: 'includeTools must be a function with signature (tool: IncludeToolsFilterToolInfo) => boolean',
});

// ===== Zod Schemas with Defaults =====

export const codeCallModeSchema = z
  .enum(['codecall_only', 'codecall_opt_in', 'metadata_driven'])
  .default('codecall_only');

export const codeCallVmPresetSchema = z.enum(['locked_down', 'secure', 'balanced', 'experimental']).default('secure');

// Default values for VM options
const DEFAULT_VM_OPTIONS = {
  preset: 'secure' as const,
};

export const codeCallVmOptionsSchema = z
  .object({
    /**
     * CSP-like preset; see README.
     * @default 'secure'
     */
    preset: codeCallVmPresetSchema,

    /**
     * Timeout for script execution in milliseconds
     * Defaults vary by preset
     */
    timeoutMs: z.number().positive().optional(),

    /**
     * Allow loop constructs (for, while, do-while)
     * Defaults vary by preset
     */
    allowLoops: z.boolean().optional(),

    /**
     * Maximum number of steps (if applicable)
     * Defaults vary by preset
     */
    maxSteps: z.number().positive().optional(),

    /**
     * List of disabled builtin functions
     * Defaults vary by preset
     */
    disabledBuiltins: z.array(z.string()).optional(),

    /**
     * List of disabled global variables
     * Defaults vary by preset
     */
    disabledGlobals: z.array(z.string()).optional(),

    /**
     * Allow console.log/warn/error
     * Defaults vary by preset
     */
    allowConsole: z.boolean().optional(),

    /**
     * Maximum nesting depth for sanitized output values
     * Prevents deeply nested objects from causing stack overflow
     * @default varies by preset (secure: 50)
     */
    maxSanitizeDepth: z.number().positive().optional(),

    /**
     * Maximum properties per object in sanitized output values
     * Limits array sizes and object keys in return values
     * @default varies by preset (secure: 2000)
     */
    maxSanitizeProperties: z.number().positive().optional(),
  })
  .default(() => DEFAULT_VM_OPTIONS);

export const codeCallDirectCallsOptionsSchema = z.object({
  /**
   * Enable/disable the `codecall.invoke` meta-tool.
   */
  enabled: z.boolean(),

  /**
   * Optional allowlist of tool names.
   */
  allowedTools: z.array(z.string()).optional(),

  /**
   * Optional advanced filter function.
   * Signature: (tool: DirectCallsFilterToolInfo) => boolean
   */
  filter: directCallsFilterSchema.optional(),
});

export const embeddingStrategySchema = z.enum(['tfidf', 'ml']).default('tfidf');

// Synonym expansion configuration schema
export const synonymExpansionConfigSchema = z
  .object({
    /**
     * Enable/disable synonym expansion for TF-IDF search.
     * When enabled, queries are expanded with synonyms to improve relevance.
     * For example, "add user" will also match tools containing "create user".
     * @default true
     */
    enabled: z.boolean().default(true),

    /**
     * Additional synonym groups beyond the defaults.
     * Each group is an array of related terms that should be treated as equivalent.
     * @example [['customer', 'client', 'buyer'], ['order', 'purchase', 'transaction']]
     */
    additionalSynonyms: z.array(z.array(z.string())).optional(),

    /**
     * Replace default synonyms entirely with additionalSynonyms.
     * @default false
     */
    replaceDefaults: z.boolean().default(false),

    /**
     * Maximum number of synonym expansions per term.
     * Prevents query explosion for terms with many synonyms.
     * @default 5
     */
    maxExpansionsPerTerm: z.number().positive().default(5),
  })
  .default({ enabled: true, replaceDefaults: false, maxExpansionsPerTerm: 5 });

// Default values for embedding options
const DEFAULT_EMBEDDING_OPTIONS = {
  strategy: 'tfidf' as const,
  modelName: 'Xenova/all-MiniLM-L6-v2',
  cacheDir: './.cache/transformers',
  useHNSW: false,
  synonymExpansion: { enabled: true, replaceDefaults: false, maxExpansionsPerTerm: 5 },
};

export const codeCallEmbeddingOptionsSchema = z
  .object({
    /**
     * Embedding strategy to use for tool search
     * - 'tfidf': Lightweight, synchronous TF-IDF based search (no ML models required)
     * - 'ml': ML-based semantic search using transformers.js (better quality, requires model download)
     * @default 'tfidf'
     */
    strategy: embeddingStrategySchema.optional(),

    /**
     * Model name for ML-based embeddings (only used when strategy='ml')
     * @default 'Xenova/all-MiniLM-L6-v2'
     */
    modelName: z.string().optional(),

    /**
     * Cache directory for ML models (only used when strategy='ml')
     * @default './.cache/transformers'
     */
    cacheDir: z.string().optional(),

    /**
     * Enable HNSW index for faster search (only used when strategy='ml')
     * When enabled, provides O(log n) search instead of O(n) brute-force
     * @default false
     */
    useHNSW: z.boolean().optional(),

    /**
     * Synonym expansion configuration for TF-IDF search.
     * When enabled, queries like "add user" will match tools for "create user".
     * Only applies when strategy is 'tfidf' (ML already handles semantic similarity).
     * Set to false to disable, or provide a config object to customize.
     * @default { enabled: true }
     */
    synonymExpansion: z.union([z.literal(false), synonymExpansionConfigSchema]).optional(),
  })
  .optional()
  .transform((opts) => ({
    strategy: opts?.strategy ?? DEFAULT_EMBEDDING_OPTIONS.strategy,
    modelName: opts?.modelName ?? DEFAULT_EMBEDDING_OPTIONS.modelName,
    cacheDir: opts?.cacheDir ?? DEFAULT_EMBEDDING_OPTIONS.cacheDir,
    useHNSW: opts?.useHNSW ?? DEFAULT_EMBEDDING_OPTIONS.useHNSW,
    synonymExpansion: opts?.synonymExpansion ?? DEFAULT_EMBEDDING_OPTIONS.synonymExpansion,
  }));

export const codeCallSidecarOptionsSchema = z
  .object({
    /**
     * Enable pass-by-reference support via sidecar
     * When enabled, large strings are automatically lifted to a sidecar
     * and resolved at the callTool boundary
     * @default false
     */
    enabled: z.boolean().default(false),

    /**
     * Maximum total size of all stored references in bytes
     * @default 16MB (from security level)
     */
    maxTotalSize: z.number().positive().optional(),

    /**
     * Maximum size of a single reference in bytes
     * @default 4MB (from security level)
     */
    maxReferenceSize: z.number().positive().optional(),

    /**
     * Threshold in bytes to trigger extraction from source code
     * Strings larger than this are lifted to the sidecar
     * @default 64KB (from security level)
     */
    extractionThreshold: z.number().positive().optional(),

    /**
     * Maximum expanded size when resolving references for tool calls
     * @default 8MB (from security level)
     */
    maxResolvedSize: z.number().positive().optional(),

    /**
     * Whether to allow composite handles from string concatenation
     * If false, concatenating references throws an error
     * @default false (strict mode)
     */
    allowComposites: z.boolean().optional(),

    /**
     * Maximum script length (in characters) when sidecar is disabled
     * Prevents large inline data from being embedded in script
     * If null, no limit is enforced
     * @default 64KB
     */
    maxScriptLengthWhenDisabled: z
      .number()
      .positive()
      .nullable()
      .default(64 * 1024),
  })
  .default(() => ({
    enabled: false,
    maxScriptLengthWhenDisabled: 64 * 1024,
  }));

// Inner schema without the outer .default() - used for extracting input type
const codeCallPluginOptionsObjectSchema = z.object({
  /**
   * CodeCall mode
   * @default 'codecall_only'
   */
  mode: codeCallModeSchema,

  /**
   * App IDs that CodeCall manages. When set, `codecall_only` mode only hides tools
   * from these apps — tools from other apps remain visible in list_tools.
   * When unset (default), all tools in the server are affected.
   * @example ['ecommerce'] — only hide ecommerce app tools, keep calc tools visible
   */
  appIds: z.array(z.string()).optional(),

  /**
   * Default number of tools to return in search results
   * @default 8
   */
  topK: z.number().positive().default(8),

  /**
   * Maximum number of tool definitions to include
   * @default 8
   */
  maxDefinitions: z.number().positive().default(8),

  /**
   * Optional filter function for including tools.
   * Signature: (tool: IncludeToolsFilterToolInfo) => boolean
   */
  includeTools: includeToolsFilterSchema.optional(),

  /**
   * Direct calls configuration
   */
  directCalls: codeCallDirectCallsOptionsSchema.optional(),

  /**
   * VM execution options
   */
  vm: codeCallVmOptionsSchema,

  /**
   * Embedding configuration for tool search
   */
  embedding: codeCallEmbeddingOptionsSchema,

  /**
   * Sidecar (pass-by-reference) configuration
   * When enabled, large data is stored outside the sandbox and resolved at callTool boundary
   */
  sidecar: codeCallSidecarOptionsSchema,
});

// Default values for plugin options
const DEFAULT_PLUGIN_OPTIONS = {
  mode: 'codecall_only' as const,
  topK: 8,
  maxDefinitions: 8,
  vm: DEFAULT_VM_OPTIONS,
  embedding: DEFAULT_EMBEDDING_OPTIONS,
  sidecar: {
    enabled: false,
    maxScriptLengthWhenDisabled: 64 * 1024,
  },
};

// Full schema with default - used for parsing
export const codeCallPluginOptionsSchema = codeCallPluginOptionsObjectSchema.prefault(DEFAULT_PLUGIN_OPTIONS);

// ===== TypeScript Types =====

// Output types (after parsing, with defaults applied) - use for internal plugin logic
export type CodeCallMode = z.infer<typeof codeCallModeSchema>;
export type CodeCallVmPreset = z.infer<typeof codeCallVmPresetSchema>;
export type CodeCallVmOptions = z.infer<typeof codeCallVmOptionsSchema>;
export type CodeCallDirectCallsOptions = z.infer<typeof codeCallDirectCallsOptionsSchema>;
export type EmbeddingStrategy = z.infer<typeof embeddingStrategySchema>;
export type SynonymExpansionConfig = z.infer<typeof synonymExpansionConfigSchema>;
export type CodeCallSidecarOptions = z.infer<typeof codeCallSidecarOptionsSchema>;

export type CodeCallEmbeddingOptions = z.infer<typeof codeCallEmbeddingOptionsSchema>;
export type CodeCallEmbeddingOptionsInput = z.input<typeof codeCallEmbeddingOptionsSchema>;

/**
 * Resolved options type (after parsing with defaults applied).
 * Use this for internal plugin logic where all defaults are guaranteed.
 */
export type CodeCallPluginOptions = z.infer<typeof codeCallPluginOptionsSchema>;

/**
 * Input options type (what users provide to init()).
 * All fields with defaults are optional.
 */
export type CodeCallPluginOptionsInput = z.input<typeof codeCallPluginOptionsObjectSchema>;

/**
 * Per-tool metadata used by CodeCall.
 * This maps to `metadata.codecall` on tools.
 */
export interface CodeCallToolMetadata {
  /**
   * If true, this tool stays visible in `list_tools`
   * even when CodeCall is hiding most tools.
   */
  visibleInListTools?: boolean;

  /**
   * Whether this tool can be used via CodeCall.
   * Semantics depend on CodeCallMode (see README).
   */
  enabledInCodeCall?: boolean;

  /** Optional extra indexing hints */
  appId?: string;
  source?: string;
  tags?: string[];
}

// ----- meta-tool contracts -----

export type CodeCallSearchInput = {
  query: string;
  topK?: number;
  filter?: {
    appIds?: string[];
    tags?: string[];
    includeOpenApi?: boolean;
    includeInline?: boolean;
  };
};

export type CodeCallSearchResult = {
  tools: {
    name: string;
    description: string;
    appId?: string;
    source?: string;
    score: number;
  }[];
};

export type CodeCallDescribeInput = {
  tools: string[];
  max?: number;
};

export type CodeCallDescribeResult = {
  tools: {
    name: string;
    description: string;
    inputSchema: unknown;
    outputSchema?: unknown | null;
    examples?: {
      input: unknown;
      output?: unknown;
    }[];
  }[];
};

export type CodeCallExecuteInput = {
  script: string;
  /**
   * Arbitrary, readonly context exposed as `codecallContext`.
   */
  context?: Record<string, unknown>;
};

// ---- global FrontMCP metadata extension ----

declare global {
  interface ExtendFrontMcpToolMetadata {
    /**
     * CodeCall-specific metadata, attached via `@Tool({ metadata: { codecall: ... } })`
     * or whatever your decorator mapping is.
     */
    codecall?: CodeCallToolMetadata;
  }
}
