import * as fs from "fs";
import * as path from "path";
import * as yaml from "js-yaml";
import { fileURLToPath } from "url";
import { dirname } from "path";
import { z } from "zod";

// In ES modules, __dirname is not available directly
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

/**
 * Represents a parameter for a tool
 *
 * @interface ParameterConfig
 */
export interface ParameterConfig {
  /** Type of the parameter */
  type: "string" | "number" | "boolean" | "array" | "object" | "enum";
  /** Description of what the parameter does */
  description?: string;
  /** Whether the parameter is required */
  required?: boolean;
  /** Default value for the parameter */
  default?: any;
  /** Possible values for enum type parameters */
  enum?: (string | number)[];
  /** For array types, defines the type of items in the array */
  items?: ParameterConfig;
  /** For object types, defines the properties of the object */
  properties?: Record<string, ParameterConfig>;
}

/**
 * Represents a tool that can be used in a prompt
 *
 * @interface ToolConfig
 */
export interface ToolConfig {
  /** Name of the tool */
  name: string;
  /** Description of what the tool does */
  description?: string;
  /** Parameters that the tool accepts */
  parameters?: Record<string, ParameterConfig>;
}

/**
 * Configuration for a specific prompt
 *
 * @interface PromptConfig
 */
export interface PromptConfig {
  /** If provided, completely replaces the default prompt */
  prompt?: string;
  /** Additional context to append to the prompt (either default or custom) */
  context?: string;
  /** Available tools for this prompt */
  tools?: ToolConfig[];
  /** Whether tools should be executed sequentially or situationally */
  toolMode?: "sequential" | "situational";
  /** Description for the tool (used as second parameter in server.tool) */
  description?: string;
  /** Whether this tool is disabled */
  disabled?: boolean;
  /** Optional name override for the registered tool (default is the config key) */
  name?: string;
  /** Parameters that the tool accepts */
  parameters?: Record<string, ParameterConfig>;
}

/**
 * Main configuration interface for all developer tools
 * All tool configurations are dynamically loaded from YAML files in the presets directory
 *
 * @interface DevToolsConfig
 */
export interface DevToolsConfig {
  /**
   * Dynamic mapping of tool names to their configurations
   * Tool names are determined by the keys in the YAML preset files
   */
  [key: string]: PromptConfig | undefined;
}

// Default empty configuration
const defaultConfig: DevToolsConfig = {};

/**
 * Merges two config objects, with the second one having precedence
 *
 * @param {DevToolsConfig} target - The target config to merge into
 * @param {DevToolsConfig} source - The source config to merge from (has precedence over target)
 * @returns {DevToolsConfig} The merged configuration object
 */
export function mergeConfigs(
  target: DevToolsConfig,
  source: DevToolsConfig
): DevToolsConfig {
  Object.entries(source).forEach(([key, value]) => {
    if (target[key]) {
      // If the property already exists, merge with the existing one
      target[key] = {
        ...target[key],
        ...value,
        // Special handling for tools array - concatenate rather than replace
        tools: mergeTools(target[key]?.tools, value?.tools),
      };
    } else {
      // Otherwise, just set it
      target[key] = value;
    }
  });

  return target;
}

/**
 * Helper function to merge tools arrays from two configs
 *
 * @param {ToolConfig[] | undefined} targetTools - The target tools array
 * @param {ToolConfig[] | undefined} sourceTools - The source tools array to merge
 * @returns {ToolConfig[] | undefined} The merged tools array or undefined if both inputs are undefined
 */
function mergeTools(
  targetTools?: ToolConfig[],
  sourceTools?: ToolConfig[]
): ToolConfig[] | undefined {
  if (!targetTools && !sourceTools) {
    return undefined;
  }
  if (!targetTools) {
    return sourceTools;
  }
  if (!sourceTools) {
    return targetTools;
  }
  return [...targetTools, ...sourceTools];
}

/**
 * Loads preset configuration from a YAML file
 *
 * @param {string} filePath - Path to the preset YAML file
 * @returns {DevToolsConfig} The loaded configuration or empty object on error
 */
function loadPresetConfig(filePath: string): DevToolsConfig {
  try {
    const content = fs.readFileSync(filePath, "utf-8");
    const config = yaml.load(content) as DevToolsConfig;

    if (typeof config !== "object") {
      console.error(
        `Preset config in ${filePath} must be an object, returning empty config`
      );
      return {};
    }

    return config;
  } catch (error: unknown) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    console.error(
      `Error loading preset config from ${filePath}: ${errorMessage}`
    );
    return {};
  }
}

/**
 * Lists all available preset names by scanning the presets directory
 *
 * @returns {string[]} Array of available preset names (without file extensions)
 */
export function listAvailablePresets(): string[] {
  try {
    const presetsDir = path.join(__dirname, "presets");
    if (!fs.existsSync(presetsDir)) {
      console.error(`Presets directory not found at ${presetsDir}`);
      return [];
    }

    return fs
      .readdirSync(presetsDir)
      .filter((file) => file.endsWith(".yaml") || file.endsWith(".yml"))
      .map((file) => path.basename(file, path.extname(file)));
  } catch (error) {
    console.error("Error listing available presets:", error);
    return [];
  }
}

/**
 * Loads configuration from preset names
 *
 * @param {string[]} presets - Array of preset names to load
 * @returns {DevToolsConfig} The merged preset configuration
 */
export function loadPresetConfigs(presets: string[]): DevToolsConfig {
  const mergedConfig: DevToolsConfig = {};
  const availablePresets = listAvailablePresets();

  for (const preset of presets) {
    try {
      // If preset is empty, skip
      if (!preset) {
        console.error("Empty preset name provided, skipping");
        continue;
      }

      // Check if preset exists
      if (!availablePresets.includes(preset)) {
        console.error(`Preset "${preset}" not found, skipping`);
        continue;
      }

      // Load preset from the internal presets directory
      const presetPath = path.join(__dirname, "presets", `${preset}.yaml`);
      const presetConfig = loadPresetConfig(presetPath);
      mergeConfigs(mergedConfig, presetConfig);
    } catch (error) {
      console.error(`Error loading preset "${preset}": ${error}`);
    }
  }

  return mergedConfig;
}

/**
 * Loads configuration from all YAML files in the specified directory
 * or returns default config if directory not found or empty
 *
 * @param {string} [directoryPath] - Path to the directory containing configuration YAML files
 * @returns {Promise<DevToolsConfig>} Promise resolving to the loaded and merged configuration
 */
export async function loadConfig(
  directoryPath?: string
): Promise<DevToolsConfig> {
  if (!directoryPath) {
    console.error(
      "No config directory path provided, using default configuration"
    );
    return defaultConfig;
  }

  try {
    // Resolve absolute path
    const absolutePath = path.resolve(directoryPath);

    // Check if directory exists and is a directory
    if (
      !fs.existsSync(absolutePath) ||
      !fs.statSync(absolutePath).isDirectory()
    ) {
      console.error(
        `Config directory not found or is not a directory at ${absolutePath}, using default configuration`
      );
      return defaultConfig;
    }

    // Check if directory name is either .workflows or .mcp-workflows
    const validDirNames = [".workflows", ".mcp-workflows"];
    const dirName = path.basename(absolutePath);
    if (!validDirNames.includes(dirName)) {
      console.error(
        `Config directory must be named either .workflows or .mcp-workflows, found ${dirName}, using default configuration`
      );
      return defaultConfig;
    }

    // Read all YAML files in the directory
    const files = fs
      .readdirSync(absolutePath)
      .filter(
        (file) =>
          file.toLowerCase().endsWith(".yaml") ||
          file.toLowerCase().endsWith(".yml")
      );

    if (files.length === 0) {
      console.error(
        `No YAML files found in ${absolutePath}, using default configuration`
      );
      return defaultConfig;
    }

    // Merge all configurations
    const mergedConfig: DevToolsConfig = {};

    for (const file of files) {
      const filePath = path.join(absolutePath, file);
      console.error(`Loading config from: ${filePath}`);

      try {
        const content = fs.readFileSync(filePath, "utf-8");
        const fileConfig = yaml.load(content) as DevToolsConfig;

        if (typeof fileConfig !== "object") {
          console.error(`Config in ${filePath} must be an object, skipping`);
          continue;
        }

        // Merge this file's config into the overall config
        mergeConfigs(mergedConfig, fileConfig);
      } catch (err: unknown) {
        const errorMessage = err instanceof Error ? err.message : String(err);
        console.error(
          `Error loading config from ${filePath}: ${errorMessage}, skipping`
        );
      }
    }

    return mergedConfig;
  } catch (error: unknown) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    console.error(`Error loading configs from directory: ${errorMessage}`);
    return defaultConfig;
  }
}

/**
 * Synchronous version of loadConfig for easier testing
 *
 * @param {string} [directoryPath] - Path to the directory containing configuration YAML files
 * @returns {DevToolsConfig} The loaded and merged configuration
 */
export function loadConfigSync(directoryPath?: string): DevToolsConfig {
  if (!directoryPath) {
    console.error(
      "No config directory path provided, using default configuration"
    );
    return defaultConfig;
  }

  try {
    // Resolve absolute path
    const absolutePath = path.resolve(directoryPath);

    // Check if directory exists and is a directory
    if (
      !fs.existsSync(absolutePath) ||
      !fs.statSync(absolutePath).isDirectory()
    ) {
      console.error(
        `Config directory not found or is not a directory at ${absolutePath}, using default configuration`
      );
      return defaultConfig;
    }

    // Check if directory name is either .workflows or .mcp-workflows
    const validDirNames = [".workflows", ".mcp-workflows"];
    const dirName = path.basename(absolutePath);
    if (!validDirNames.includes(dirName)) {
      console.error(
        `Config directory must be named either .workflows or .mcp-workflows, found ${dirName}, using default configuration`
      );
      return defaultConfig;
    }

    // Read all YAML files in the directory
    const files = fs
      .readdirSync(absolutePath)
      .filter(
        (file) =>
          file.toLowerCase().endsWith(".yaml") ||
          file.toLowerCase().endsWith(".yml")
      );

    if (files.length === 0) {
      console.error(
        `No YAML files found in ${absolutePath}, using default configuration`
      );
      return defaultConfig;
    }

    // Merge all configurations
    const mergedConfig: DevToolsConfig = {};

    for (const file of files) {
      const filePath = path.join(absolutePath, file);
      console.error(`Loading config from: ${filePath}`);

      try {
        const content = fs.readFileSync(filePath, "utf-8");
        const fileConfig = yaml.load(content) as DevToolsConfig;

        if (typeof fileConfig !== "object") {
          console.error(`Config in ${filePath} must be an object, skipping`);
          continue;
        }

        // Merge this file's config into the overall config
        mergeConfigs(mergedConfig, fileConfig);
      } catch (err: unknown) {
        const errorMessage = err instanceof Error ? err.message : String(err);
        console.error(
          `Error loading config from ${filePath}: ${errorMessage}, skipping`
        );
      }
    }

    return mergedConfig;
  } catch (error: unknown) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    console.error(`Error loading configs from directory: ${errorMessage}`);
    return defaultConfig;
  }
}

/**
 * Validates the configuration of a specific tool
 * Note: This function validates the tool definition itself, not the runtime parameters
 * (which are validated by the schema validation mechanism)
 *
 * @param {DevToolsConfig} config - The complete tool configuration
 * @param {string} toolName - The name of the tool to validate
 * @returns {string|null} Error message if invalid, null if valid
 */
export function validateToolConfig(
  config: DevToolsConfig,
  toolName: string
): string | null {
  try {
    // Get the tool configuration
    const toolConfig = config[toolName];
    if (!toolConfig) {
      return `Tool "${toolName}" not found in configuration`;
    }

    // If the tool has parameters, validate them
    if (toolConfig.parameters) {
      for (const [paramName, param] of Object.entries(toolConfig.parameters)) {
        // Check parameter type
        if (!param.type) {
          return `Parameter "${paramName}" is missing type property`;
        }

        const validTypes = [
          "string",
          "number",
          "boolean",
          "array",
          "object",
          "enum",
        ];
        if (!validTypes.includes(param.type)) {
          return `Parameter "${paramName}" has invalid type "${param.type}"`;
        }

        // For enum types, check that enum values are provided
        if (param.type === "enum") {
          if (
            !param.enum ||
            !Array.isArray(param.enum) ||
            param.enum.length === 0
          ) {
            return `Parameter "${paramName}" of type "enum" must have a non-empty enum array`;
          }
        }

        // Recursively validate nested parameters
        if (param.type === "object" && param.properties) {
          for (const [nestedName, nestedParam] of Object.entries(
            param.properties
          )) {
            if (!nestedParam.type) {
              return `Nested parameter "${nestedName}" in "${paramName}" is missing type property`;
            }

            if (!validTypes.includes(nestedParam.type)) {
              return `Nested parameter "${nestedName}" in "${paramName}" has invalid type "${nestedParam.type}"`;
            }

            // Recursively validate deeper nested structures
            const nestedValidation = validateNestedParameter(
              nestedParam,
              `${paramName}.${nestedName}`
            );
            if (nestedValidation) {
              return nestedValidation;
            }
          }
        }

        // Validate array item types
        if (param.type === "array" && param.items) {
          if (!param.items.type) {
            return `Items in array parameter "${paramName}" must specify a type`;
          }

          if (!validTypes.includes(param.items.type)) {
            return `Items in array parameter "${paramName}" have invalid type "${param.items.type}"`;
          }

          // Recursively validate array item if it's a complex type
          const itemValidation = validateNestedParameter(
            param.items,
            `${paramName} items`
          );
          if (itemValidation) {
            return itemValidation;
          }
        }
      }
    }

    return null;
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    return `Error validating tool configuration: ${errorMessage}`;
  }
}

/**
 * Helper function to recursively validate a nested parameter
 * @param {ParameterConfig} param - Parameter configuration to validate
 * @param {string} path - Path to the parameter (for error reporting)
 * @returns {string|null} Error message if invalid, null if valid
 */
function validateNestedParameter(
  param: ParameterConfig,
  path: string
): string | null {
  const validTypes = ["string", "number", "boolean", "array", "object", "enum"];

  // For enum types, check that enum values are provided
  if (param.type === "enum") {
    if (!param.enum || !Array.isArray(param.enum) || param.enum.length === 0) {
      return `Parameter "${path}" of type "enum" must have a non-empty enum array`;
    }
  }

  // Recursively validate nested objects
  if (param.type === "object" && param.properties) {
    for (const [nestedName, nestedParam] of Object.entries(param.properties)) {
      if (!nestedParam.type) {
        return `Nested parameter "${nestedName}" in "${path}" is missing type property`;
      }

      if (!validTypes.includes(nestedParam.type)) {
        return `Nested parameter "${nestedName}" in "${path}" has invalid type "${nestedParam.type}"`;
      }

      const nestedValidation = validateNestedParameter(
        nestedParam,
        `${path}.${nestedName}`
      );
      if (nestedValidation) {
        return nestedValidation;
      }
    }
  }

  // Validate array item types
  if (param.type === "array" && param.items) {
    if (!param.items.type) {
      return `Items in array parameter "${path}" must specify a type`;
    }

    if (!validTypes.includes(param.items.type)) {
      return `Items in array parameter "${path}" have invalid type "${param.items.type}"`;
    }

    const itemValidation = validateNestedParameter(
      param.items,
      `${path} items`
    );
    if (itemValidation) {
      return itemValidation;
    }
  }

  return null;
}

/**
 * Converts parameter configuration to JSON Schema format
 * @param {Record<string, ParameterConfig>} parameters - Parameter configurations
 * @returns {object} JSON Schema object
 */
export function convertParametersToJsonSchema(
  parameters: Record<string, ParameterConfig>
): any {
  const properties: Record<string, any> = {};
  const required: string[] = [];

  for (const [name, param] of Object.entries(parameters)) {
    if (param.required) {
      required.push(name);
    }

    properties[name] = convertParameterToJsonSchema(param);
  }

  // Fix the schema format to be compatible with MCP SDK
  const schema = {
    type: "object",
    properties,
    ...(required.length > 0 ? { required } : {}),
  };

  return schema;
}

/**
 * Converts a single parameter configuration to JSON Schema
 * @param {ParameterConfig} param - Parameter configuration
 * @returns {object} JSON Schema for the parameter
 */
export function convertParameterToJsonSchema(param: ParameterConfig): any {
  const schema: any = {};

  switch (param.type) {
    case "string":
      schema.type = "string";
      break;
    case "number":
      schema.type = "number";
      break;
    case "boolean":
      schema.type = "boolean";
      break;
    case "array":
      schema.type = "array";
      if (param.items) {
        schema.items = convertParameterToJsonSchema(param.items);
      } else {
        schema.items = { type: "string" };
      }
      break;
    case "object":
      schema.type = "object";
      if (param.properties) {
        const nestedSchema = convertParametersToJsonSchema(param.properties);
        schema.properties = nestedSchema.properties;
        if (nestedSchema.required && nestedSchema.required.length > 0) {
          schema.required = nestedSchema.required;
        }
      } else {
        schema.additionalProperties = true;
      }
      break;
    case "enum":
      if (param.enum && param.enum.length > 0) {
        const firstValue = param.enum[0];
        if (typeof firstValue === "number") {
          schema.type = "number";
        } else {
          schema.type = "string";
        }
        schema.enum = param.enum;
      } else {
        schema.type = "string";
        schema.enum = [];
      }
      break;
  }

  if (param.description) {
    schema.description = param.description;
  }

  if (param.default !== undefined) {
    schema.default = param.default;
  }

  return schema;
}

/**
 * Converts parameter configuration to Zod schema
 * @param {Record<string, ParameterConfig>} parameters - Parameter configurations
 * @returns {object} Zod schema object for use with MCP SDK
 */
export function convertParametersToZodSchema(
  parameters: Record<string, ParameterConfig>
): Record<string, z.ZodTypeAny> {
  const schemaObj: Record<string, z.ZodTypeAny> = {};

  for (const [name, param] of Object.entries(parameters)) {
    let schema = convertParameterToZodSchema(param);

    // If parameter is required, don't add .optional()
    if (!param.required) {
      schema = schema.optional();
    }

    schemaObj[name] = schema;
  }

  return schemaObj;
}

/**
 * Converts a single parameter configuration to a Zod schema
 * @param {ParameterConfig} param - Parameter configuration
 * @returns {z.ZodTypeAny} Zod schema for the parameter
 */
export function convertParameterToZodSchema(
  param: ParameterConfig
): z.ZodTypeAny {
  let schema: z.ZodTypeAny;

  switch (param.type) {
    case "string":
      schema = z.string();
      break;
    case "number":
      schema = z.number();
      break;
    case "boolean":
      schema = z.boolean();
      break;
    case "array":
      if (param.items) {
        // Create array with the specific item type
        schema = z.array(convertParameterToZodSchema(param.items));
      } else {
        // Default to array of strings if item type not specified
        schema = z.array(z.string());
      }
      break;
    case "object":
      if (param.properties) {
        // Create object with specific properties
        const propertySchemas = convertParametersToZodSchema(param.properties);
        schema = z.object(propertySchemas);
      } else {
        // Default to record of unknown if properties not specified
        schema = z.record(z.unknown());
      }
      break;
    case "enum":
      if (param.enum && param.enum.length > 0) {
        const firstValue = param.enum[0];
        if (typeof firstValue === "number") {
          // For numeric enums, we need to handle them differently
          // Since z.nativeEnum expects an actual TypeScript enum,
          // we'll use z.union of z.literal values
          schema = z.union(
            param.enum.map((value) => z.literal(value)) as [
              z.ZodLiteral<any>,
              z.ZodLiteral<any>,
              ...z.ZodLiteral<any>[]
            ]
          );
        } else {
          // For string enums, use regular enum
          schema = z.enum(param.enum as [string, ...string[]]);
        }
      } else {
        // Default to empty string enum if enum values not provided
        schema = z.enum([""] as [string, ...string[]]);
      }
      break;
    default:
      // Default to string for unknown types
      schema = z.string();
  }

  // Add description if available
  if (param.description) {
    schema = schema.describe(param.description);
  }

  // Add default value if specified
  if (param.default !== undefined) {
    schema = schema.default(param.default);
  }

  return schema;
}
