// @lifecycle canonical - Runtime YAML loading for script tools (mirrors GateDefinitionLoader)
/**
 * Script Tool Definition Loader
 *
 * Loads script tool definitions from YAML source files at runtime,
 * following the same pattern as GateDefinitionLoader.
 *
 * Structure:
 * ```
 * prompts/{category}/{prompt_id}/
 * └── tools/
 *     └── {tool_id}/
 *         ├── tool.yaml      # Main configuration
 *         ├── schema.json    # JSON Schema for inputs
 *         ├── description.md # Tool description
 *         └── script.py      # Executable
 * ```
 *
 * Features:
 * - Runtime YAML parsing via shared utilities
 * - Automatic inlining of schema.json and description.md
 * - Validation of definitions on load
 * - Configurable caching for performance
 * - Per-prompt tool discovery
 *
 * @see GateDefinitionLoader for the pattern this follows
 */

import { existsSync, readdirSync, readFileSync } from 'fs';
import { join, basename } from 'path';

import {
  validateScriptToolSchema,
  type ScriptToolSchemaValidationResult,
  type ScriptToolYaml,
} from './script-schema.js';
import { loadYamlFileSync } from '../../../shared/utils/yaml/index.js';
import { DEFAULT_EXECUTION_CONFIG } from '../types.js';

import type {
  LoadedScriptTool,
  ScriptToolLoaderConfig,
  ScriptToolLoaderStats,
  JSONSchemaDefinition,
  ExecutionConfig,
} from '../types.js';

// Re-export validation types
export type { ScriptToolSchemaValidationResult } from './script-schema.js';

/**
 * Script Tool Definition Loader
 *
 * Provides runtime loading of script tool definitions from YAML source files.
 * Unlike GateDefinitionLoader which uses a global directory, this loader
 * works on a per-prompt basis, loading tools from the prompt's tools/ directory.
 *
 * @example
 * ```typescript
 * const loader = new ScriptToolDefinitionLoader();
 *
 * // Discover tools for a specific prompt
 * const toolIds = loader.discoverTools('/path/to/prompts/analysis/data_analyzer');
 * // ['analyze_csv', 'generate_chart']
 *
 * // Load a specific tool
 * const tool = loader.loadTool('/path/to/prompts/analysis/data_analyzer', 'analyze_csv');
 *
 * // Load all tools declared by a prompt
 * const tools = loader.loadToolsForPrompt('/path/to/prompts/analysis/data_analyzer', ['analyze_csv']);
 * ```
 */
export class ScriptToolDefinitionLoader {
  private cache = new Map<string, LoadedScriptTool>();
  private stats = { cacheHits: 0, cacheMisses: 0, loadErrors: 0 };
  private enableCache: boolean;
  private validateOnLoad: boolean;
  private debug: boolean;

  constructor(config: ScriptToolLoaderConfig = {}) {
    this.enableCache = config.enableCache ?? true;
    this.validateOnLoad = config.validateOnLoad ?? true;
    this.debug = config.debug ?? false;

    if (this.debug) {
      process.stderr.write('[ScriptToolDefinitionLoader] Initialized\n');
    }
  }

  /**
   * Load a script tool definition by ID from a prompt directory.
   *
   * @param promptDir - Absolute path to the prompt directory
   * @param toolId - Tool ID (directory name under tools/)
   * @param promptId - ID of the parent prompt (for LoadedScriptTool)
   * @returns Loaded tool definition or undefined if not found
   */
  loadTool(promptDir: string, toolId: string, promptId: string): LoadedScriptTool | undefined {
    const normalizedId = toolId.toLowerCase();
    const toolDir = join(promptDir, 'tools', normalizedId);
    const cacheKey = toolDir; // Use absolute path as cache key for uniqueness

    // Check cache first
    if (this.enableCache && this.cache.has(cacheKey)) {
      this.stats.cacheHits++;
      return this.cache.get(cacheKey);
    }

    this.stats.cacheMisses++;

    // Load from YAML directory
    const definition = this.loadFromToolDir(toolDir, normalizedId, promptId);

    if (!definition) {
      return undefined;
    }

    // Cache result
    if (this.enableCache) {
      this.cache.set(cacheKey, definition);
    }

    return definition;
  }

  /**
   * Discover all available tool IDs in a prompt's tools/ directory.
   *
   * @param promptDir - Absolute path to the prompt directory
   * @returns Array of tool IDs found
   */
  discoverTools(promptDir: string): string[] {
    const toolsDir = join(promptDir, 'tools');

    if (!existsSync(toolsDir)) {
      return [];
    }

    try {
      const entries = readdirSync(toolsDir, { withFileTypes: true });
      return entries
        .filter((entry) => {
          if (!entry.isDirectory()) return false;
          // Check for tool.yaml entry point
          const toolYamlPath = join(toolsDir, entry.name, 'tool.yaml');
          return existsSync(toolYamlPath);
        })
        .map((entry) => entry.name.toLowerCase())
        .sort();
    } catch (error) {
      if (this.debug) {
        process.stderr.write(
          `[ScriptToolDefinitionLoader] Failed to discover tools in ${toolsDir}: ${String(error)}\n`
        );
      }
      return [];
    }
  }

  /**
   * Load multiple tools for a prompt.
   *
   * @param promptDir - Absolute path to the prompt directory
   * @param toolIds - Array of tool IDs to load
   * @param promptId - ID of the parent prompt
   * @returns Array of successfully loaded tools
   */
  loadToolsForPrompt(promptDir: string, toolIds: string[], promptId: string): LoadedScriptTool[] {
    const results: LoadedScriptTool[] = [];

    for (const toolId of toolIds) {
      const tool = this.loadTool(promptDir, toolId, promptId);
      if (tool) {
        results.push(tool);
      } else if (this.debug) {
        process.stderr.write(
          `[ScriptToolDefinitionLoader] Tool '${toolId}' not found for prompt '${promptId}'\n`
        );
      }
    }

    return results;
  }

  /**
   * Load all available tools for a prompt (discovery + load).
   *
   * @param promptDir - Absolute path to the prompt directory
   * @param promptId - ID of the parent prompt
   * @returns Array of all successfully loaded tools
   */
  loadAllToolsForPrompt(promptDir: string, promptId: string): LoadedScriptTool[] {
    const toolIds = this.discoverTools(promptDir);
    return this.loadToolsForPrompt(promptDir, toolIds, promptId);
  }

  /**
   * Check if a tool exists in a prompt's tools/ directory.
   *
   * @param promptDir - Absolute path to the prompt directory
   * @param toolId - Tool ID to check
   * @returns True if the tool has a valid entry point
   */
  toolExists(promptDir: string, toolId: string): boolean {
    const normalizedId = toolId.toLowerCase();
    const toolYamlPath = join(promptDir, 'tools', normalizedId, 'tool.yaml');
    return existsSync(toolYamlPath);
  }

  /**
   * Clear the cache (all or for a specific prompt directory).
   *
   * @param promptDir - Optional prompt directory to clear cache for
   */
  clearCache(promptDir?: string): void {
    if (promptDir) {
      // Clear all entries that start with this prompt's tools directory
      const toolsPrefix = join(promptDir, 'tools');
      for (const key of this.cache.keys()) {
        if (key.startsWith(toolsPrefix)) {
          this.cache.delete(key);
        }
      }
    } else {
      this.cache.clear();
    }
  }

  /**
   * Clear cache for a specific tool.
   *
   * @param promptDir - Absolute path to the prompt directory
   * @param toolId - Tool ID to clear
   */
  clearToolCache(promptDir: string, toolId: string): void {
    const normalizedId = toolId.toLowerCase();
    const cacheKey = join(promptDir, 'tools', normalizedId);
    this.cache.delete(cacheKey);
  }

  /**
   * Get loader statistics.
   */
  getStats(): ScriptToolLoaderStats {
    return {
      cacheSize: this.cache.size,
      cacheHits: this.stats.cacheHits,
      cacheMisses: this.stats.cacheMisses,
      loadErrors: this.stats.loadErrors,
    };
  }

  // ============================================================================
  // Private Implementation - YAML/File Loading
  // ============================================================================

  /**
   * Load a tool from its directory (tools/{id}/).
   */
  private loadFromToolDir(
    toolDir: string,
    toolId: string,
    promptId: string
  ): LoadedScriptTool | undefined {
    try {
      const entryPath = join(toolDir, 'tool.yaml');

      if (!existsSync(entryPath)) {
        if (this.debug) {
          process.stderr.write(`[ScriptToolDefinitionLoader] tool.yaml not found: ${entryPath}\n`);
        }
        return undefined;
      }

      // Load main tool.yaml
      const yamlDefinition = loadYamlFileSync<ScriptToolYaml>(entryPath, {
        required: true,
      });

      if (!yamlDefinition) {
        return undefined;
      }

      // Inline referenced files
      const { inputSchema, descriptionContent } = this.loadReferencedFiles(toolDir, yamlDefinition);

      // Validate if enabled
      if (this.validateOnLoad) {
        const validation = this.validateDefinition(yamlDefinition, toolId);
        if (!validation.valid) {
          this.stats.loadErrors++;
          process.stderr.write(
            `[ScriptToolDefinitionLoader] Validation failed for '${toolId}': ${validation.errors.join(
              '; '
            )}\n`
          );
          return undefined;
        }
        if (validation.warnings.length > 0 && this.debug) {
          process.stderr.write(
            `[ScriptToolDefinitionLoader] Warnings for '${toolId}': ${validation.warnings.join(
              '; '
            )}\n`
          );
        }
      }

      // Resolve execution config with defaults
      const executionConfig = this.resolveExecutionConfig(yamlDefinition.execution);

      // Build LoadedScriptTool - only include optional properties when defined
      const loadedTool: LoadedScriptTool = {
        id: yamlDefinition.id,
        name: yamlDefinition.name,
        description: yamlDefinition.description ?? descriptionContent ?? '',
        scriptPath: yamlDefinition.script,
        runtime: yamlDefinition.runtime ?? 'auto',
        inputSchema,
        execution: executionConfig,
        // Resolved paths
        toolDir,
        absoluteScriptPath: join(toolDir, yamlDefinition.script),
        promptId,
        descriptionContent: descriptionContent ?? yamlDefinition.description ?? '',
        // Only include optional properties when defined
        ...(yamlDefinition.timeout !== undefined && { timeout: yamlDefinition.timeout }),
        ...(yamlDefinition.enabled !== undefined && { enabled: yamlDefinition.enabled }),
        ...(yamlDefinition.env !== undefined && { env: yamlDefinition.env }),
        ...(yamlDefinition.workingDir !== undefined && { workingDir: yamlDefinition.workingDir }),
      };

      if (this.debug) {
        process.stderr.write(
          `[ScriptToolDefinitionLoader] Loaded: ${loadedTool.name} (${toolId}) for prompt ${promptId}\n`
        );
      }

      return loadedTool;
    } catch (error) {
      this.stats.loadErrors++;
      if (this.debug) {
        process.stderr.write(
          `[ScriptToolDefinitionLoader] Failed to load '${toolId}': ${String(error)}\n`
        );
      }
      return undefined;
    }
  }

  /**
   * Load and inline referenced files (schema.json, description.md).
   */
  private loadReferencedFiles(
    toolDir: string,
    definition: ScriptToolYaml
  ): { inputSchema: JSONSchemaDefinition; descriptionContent: string | undefined } {
    let inputSchema: JSONSchemaDefinition = { type: 'object', properties: {} };
    let descriptionContent: string | undefined = undefined;

    // Load schema.json
    const schemaPath = join(toolDir, definition.schemaFile ?? 'schema.json');
    if (existsSync(schemaPath)) {
      try {
        const schemaContent = readFileSync(schemaPath, 'utf-8');
        inputSchema = JSON.parse(schemaContent) as JSONSchemaDefinition;
        if (this.debug) {
          process.stderr.write(`[ScriptToolDefinitionLoader] Loaded schema from ${schemaPath}\n`);
        }
      } catch (error) {
        process.stderr.write(
          `[ScriptToolDefinitionLoader] Failed to parse schema.json: ${String(error)}\n`
        );
      }
    } else if (this.debug) {
      process.stderr.write(`[ScriptToolDefinitionLoader] No schema.json found at ${schemaPath}\n`);
    }

    // Load description.md
    const descPath = join(toolDir, definition.descriptionFile ?? 'description.md');
    if (existsSync(descPath)) {
      try {
        descriptionContent = readFileSync(descPath, 'utf-8').trim();
        if (this.debug) {
          process.stderr.write(
            `[ScriptToolDefinitionLoader] Loaded description from ${descPath}\n`
          );
        }
      } catch (error) {
        process.stderr.write(
          `[ScriptToolDefinitionLoader] Failed to read description.md: ${String(error)}\n`
        );
      }
    }

    return { inputSchema, descriptionContent };
  }

  /**
   * Validate a tool definition using Zod schema.
   */
  private validateDefinition(
    definition: ScriptToolYaml,
    expectedId: string
  ): ScriptToolSchemaValidationResult {
    return validateScriptToolSchema(definition, expectedId);
  }

  /**
   * Resolve execution config with defaults applied.
   *
   * Handles deprecation and migration of:
   * - 'parameter_match' trigger (use 'schema_match')
   * - 'confidence' field (use 'strict' for matching control)
   * - 'mode' field (use 'trigger: explicit' or 'confirm: true')
   *
   * Migration from deprecated mode field:
   * - mode: manual  → trigger: explicit
   * - mode: confirm → confirm: true
   *
   * @param yamlConfig - Execution config from YAML (may be undefined or partial)
   * @returns Complete execution config with defaults
   */
  private resolveExecutionConfig(yamlConfig: ScriptToolYaml['execution']): ExecutionConfig {
    if (!yamlConfig) {
      return { ...DEFAULT_EXECUTION_CONFIG };
    }

    // The Zod schema transforms 'parameter_match' -> 'schema_match' and logs deprecation warning
    // So yamlConfig.trigger is already transformed if it was 'parameter_match'
    let trigger = yamlConfig.trigger ?? DEFAULT_EXECUTION_CONFIG.trigger;
    let confirm = yamlConfig.confirm ?? DEFAULT_EXECUTION_CONFIG.confirm ?? false;

    // Handle deprecated mode field migration
    if (yamlConfig.mode !== undefined && yamlConfig.mode !== 'auto') {
      if (yamlConfig.mode === 'manual' && yamlConfig.trigger === undefined) {
        // mode: manual → trigger: explicit
        trigger = 'explicit';
      } else if (yamlConfig.mode === 'confirm' && yamlConfig.confirm === undefined) {
        // mode: confirm → confirm: true
        confirm = true;
      }
    }

    // Build config with new schema (no mode field)
    const config: ExecutionConfig = {
      trigger,
      confirm,
      strict: yamlConfig.strict ?? DEFAULT_EXECUTION_CONFIG.strict,
    };

    // Add optional confirmMessage only if defined
    if (yamlConfig.confirmMessage !== undefined) {
      config.confirmMessage = yamlConfig.confirmMessage;
    }

    // Add autoApproveOnValid if defined
    if (yamlConfig.autoApproveOnValid !== undefined) {
      config.autoApproveOnValid = yamlConfig.autoApproveOnValid;
    }

    return config;
  }
}

/**
 * Factory function with default configuration.
 */
export function createScriptToolDefinitionLoader(
  config?: ScriptToolLoaderConfig
): ScriptToolDefinitionLoader {
  return new ScriptToolDefinitionLoader(config);
}

// ============================================================================
// Default Instance Management (singleton pattern)
// ============================================================================

let defaultLoader: ScriptToolDefinitionLoader | null = null;

/**
 * Get the default ScriptToolDefinitionLoader instance.
 * Creates one if it doesn't exist.
 */
export function getDefaultScriptToolDefinitionLoader(): ScriptToolDefinitionLoader {
  if (!defaultLoader) {
    defaultLoader = new ScriptToolDefinitionLoader();
  }
  return defaultLoader;
}

/**
 * Reset the default loader (useful for testing).
 */
export function resetDefaultScriptToolDefinitionLoader(): void {
  defaultLoader = null;
}
