/**
 * @fileoverview Tool registry for managing MCP tool definitions and handlers
 * @packageDocumentation
 */

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { promises as fs } from 'fs';
import { join, normalize, resolve } from 'path';
import { createLogger } from '../utils/logging/logger.js';
import { HandlerFunction, BaseHandlerDeps } from '../handlers/base/handler.interface.js';
import {
  createDefaultHandlerDeps,
  isApiResponse,
  createErrorResponse,
} from '../handlers/base/handler.factory.js';
import { logToolInvocation, logToolResult, logAndFormatError } from './tool-helpers.js';
import { isFeatureEnabled } from '../config/features.js';
import type { DiscoveryConfig } from '../config/default.js';

const logger = createLogger('ToolRegistry');

/**
 * MCP response type
 */
type McpResponse = {
  content: Array<{
    type: 'text';
    text: string;
    [key: string]: unknown;
  }>;
  isError?: boolean;
  structuredContent?: Record<string, unknown>;
  [key: string]: unknown;
};

/**
 * Tool metadata for categorization and filtering
 */
export interface ToolMetadata {
  /** Tool category for grouping */
  category?: string;
  /** Tool version */
  version?: string;
  /** Tool tags for filtering */
  tags?: string[];
  /** Whether the tool is enabled by default */
  enabled?: boolean;
}

/**
 * Tool definition interface
 */
export interface ToolDefinition<TInput = unknown, TOutput = unknown> {
  /** Tool name */
  name: string;
  /** Tool description */
  description: string;
  /** Input schema using Zod - can be ZodType or ZodRawShape */
  inputSchema?: z.ZodType<TInput> | z.ZodRawShape;
  /** Output schema using Zod - can be ZodType or ZodRawShape */
  outputSchema?: z.ZodType<TOutput> | z.ZodRawShape;
  /** Handler function */
  handler: HandlerFunction<TInput>;
  /** Optional metadata for enhanced features */
  metadata?: ToolMetadata;
}

/**
 * Tool discovery options (compatible with DiscoveryConfig)
 */
export type ToolDiscoveryOptions = Partial<DiscoveryConfig>;

/**
 * Registry for managing MCP tools
 */
export class ToolRegistry {
  private tools = new Map<string, ToolDefinition<unknown, unknown>>();
  private toolMetadata = new Map<string, ToolMetadata>();
  private discoveredTools = new Map<string, string>(); // tool name -> file path
  private server: McpServer;
  private defaultDeps: BaseHandlerDeps;

  constructor(server: McpServer, defaultDeps?: BaseHandlerDeps) {
    this.server = server;
    this.defaultDeps = defaultDeps || createDefaultHandlerDeps();
    logger.info('ToolRegistry initialized');
  }

  /**
   * Registers a tool with the MCP server
   * @param tool - The tool definition
   */
  registerTool<TInput = unknown, TOutput = unknown>(tool: ToolDefinition<TInput, TOutput>): void {
    if (this.tools.has(tool.name)) {
      logger.warn(`Tool ${tool.name} is already registered, overwriting`);
    }

    // Store metadata if provided
    if (tool.metadata) {
      this.toolMetadata.set(tool.name, tool.metadata);
      logger.debug(`Stored metadata for tool: ${tool.name}`, tool.metadata);
    }

    this.tools.set(tool.name, tool as ToolDefinition<unknown, unknown>);
    logger.debug(`Registering tool: ${tool.name}`, {
      hasInputSchema: Boolean(tool.inputSchema),
      inputSchemaType: tool.inputSchema ? typeof tool.inputSchema : 'undefined',
      inputSchemaDetails: tool.inputSchema
        ? {
            isZodSchema: tool.inputSchema && '_def' in (tool.inputSchema as object),
            schemaKeys: tool.inputSchema ? Object.keys(tool.inputSchema) : [],
          }
        : null,
    });

    // MCP SDK expects Zod schemas directly
    const toolConfig: Record<string, unknown> = {
      description: tool.description,
    };

    if (tool.inputSchema) {
      toolConfig.inputSchema = tool.inputSchema;
      logger.debug(`Tool ${tool.name} inputSchema details`, {
        schemaType: typeof tool.inputSchema,
        isZodSchema:
          tool.inputSchema && typeof tool.inputSchema === 'object' && '_def' in tool.inputSchema,
        hasShape:
          tool.inputSchema && typeof tool.inputSchema === 'object' && 'shape' in tool.inputSchema,
      });
    }

    if (tool.outputSchema) {
      toolConfig.outputSchema = tool.outputSchema;
      logger.debug(`Tool ${tool.name} outputSchema details`, {
        schemaType: typeof tool.outputSchema,
        isZodSchema:
          tool.outputSchema && typeof tool.outputSchema === 'object' && '_def' in tool.outputSchema,
      });
    }

    // Register with MCP server
    logger.info(`About to register tool with MCP server: ${tool.name}`, {
      mcpServerType: typeof this.server,
      mcpServerHasRegisterTool: 'registerTool' in this.server,
      toolConfigKeys: Object.keys(toolConfig),
    });

    try {
      this.server.registerTool(
        tool.name,
        toolConfig,

        async (params: Record<string, unknown>, _: unknown): Promise<McpResponse> => {
          // Second parameter required by MCP SDK but not used
          logger.info(`===== TOOL INVOCATION START: ${tool.name} =====`);
          logger.info(`Tool ${tool.name} received params:`, {
            params,
            paramsType: typeof params,
            paramsKeys: params ? Object.keys(params) : [],
            paramsStringified: JSON.stringify(params),
            hasInputSchema: Boolean(tool.inputSchema),
          });
          try {
            logger.debug(`Tool ${tool.name} invoked`, {
              params,
              paramsType: typeof params,
              hasInputSchema: Boolean(tool.inputSchema),
            });
            logToolInvocation(tool.name, params);

            // Validate input if schema provided
            let validatedParams: TInput;
            if (tool.inputSchema && params !== undefined) {
              logger.debug(`Validating params for ${tool.name}`, {
                schemaType: typeof tool.inputSchema,
                hasDefProperty: '_def' in (tool.inputSchema as object),
                hasSafeParseMethod: 'safeParse' in (tool.inputSchema as object),
              });

              // Wrap the inputSchema in z.object() if it's a ZodRawShape
              const schema =
                tool.inputSchema &&
                typeof tool.inputSchema === 'object' &&
                !('safeParse' in tool.inputSchema)
                  ? z.object(tool.inputSchema as z.ZodRawShape)
                  : (tool.inputSchema as z.ZodSchema);

              const parseResult = schema.safeParse(params);
              if (!parseResult.success) {
                logger.error(`Input validation failed for tool ${tool.name}`, {
                  errors: parseResult.error.issues,
                });
                throw new Error(`Invalid input: ${parseResult.error.message}`);
              }
              validatedParams = parseResult.data;
            } else {
              // If no schema or params is undefined, pass through as-is
              validatedParams = (params ?? {}) as TInput;
            }

            // Execute handler
            logger.info(`About to execute handler for ${tool.name} with validated params:`, {
              validatedParams,
              handlerType: typeof tool.handler,
            });

            const result = await tool.handler(validatedParams);

            logger.info(`Handler for ${tool.name} returned result:`, {
              resultType: typeof result,
              isApiResponse: isApiResponse(result),
              result,
            });

            logToolResult(tool.name, result);

            // Handle ApiResponse format
            if (isApiResponse(result)) {
              // If it's an error response, handle appropriately
              if (result.isError) {
                const errorContent = result.content[0];
                if (errorContent?.type === 'text') {
                  let errorData;
                  try {
                    errorData = JSON.parse(errorContent.text);
                  } catch {
                    errorData = { error: errorContent.text };
                  }
                  logger.error(`${tool.name} handler returned error`, errorData);
                  throw new Error(errorContent.text);
                }
              }

              // Parse the JSON content for structured response
              const textContent = result.content[0];
              if (textContent?.type === 'text') {
                let parsedData;
                try {
                  parsedData = JSON.parse(textContent.text);
                } catch {
                  parsedData = textContent.text;
                }

                logger.info(`Successfully processed ${tool.name}`, {
                  success: true,
                  hasData: parsedData !== null && parsedData !== undefined,
                });

                // Wrap arrays in an object for MCP SDK compatibility
                // Use tool-specific field names for arrays
                const structuredData = Array.isArray(parsedData)
                  ? { [tool.name]: parsedData }
                  : parsedData;

                const finalResponse = {
                  content: result.content,
                  structuredContent: structuredData as Record<string, unknown>,
                  isError: false,
                } as McpResponse;

                logger.info(`===== TOOL INVOCATION SUCCESS: ${tool.name} =====`, {
                  responseType: 'ApiResponse',
                  finalResponse,
                });

                return finalResponse;
              }

              logger.info(`===== TOOL INVOCATION SUCCESS: ${tool.name} =====`, {
                responseType: 'ApiResponse-passthrough',
                result,
              });

              return result as unknown as McpResponse;
            }

            // For non-ApiResponse results, wrap them
            // Wrap arrays in an object for MCP SDK compatibility
            // Use tool-specific field names for arrays
            const structuredResult = Array.isArray(result) ? { [tool.name]: result } : result;

            const wrappedResponse = {
              content: [
                {
                  type: 'text',
                  text: JSON.stringify(result, null, 2),
                },
              ],
              structuredContent: structuredResult as Record<string, unknown>,
              isError: false,
            } as McpResponse;

            logger.info(`===== TOOL INVOCATION SUCCESS: ${tool.name} =====`, {
              responseType: 'wrapped',
              wrappedResponse,
            });

            return wrappedResponse;
          } catch (error) {
            logger.error(`===== TOOL INVOCATION ERROR: ${tool.name} =====`, {
              error,
              errorMessage: error instanceof Error ? error.message : String(error),
              errorStack: error instanceof Error ? error.stack : undefined,
            });

            const errorMessage = logAndFormatError(error, tool.name);
            const errorResponse = createErrorResponse(error, `Failed to execute ${tool.name}`);

            // Extract structured error data
            let structuredError = {};
            try {
              const errorContent = errorResponse.content[0];
              if (errorContent?.type === 'text') {
                structuredError = JSON.parse(errorContent.text);
              }
            } catch {
              structuredError = { error: errorMessage };
            }

            const finalErrorResponse = {
              content: [
                {
                  type: 'text',
                  text: errorMessage,
                },
              ],
              structuredContent: structuredError as Record<string, unknown>,
              isError: true,
            } as McpResponse;

            logger.error(`===== TOOL INVOCATION FAILED: ${tool.name} =====`, {
              finalErrorResponse,
            });

            return finalErrorResponse;
          }
        }
      );

      logger.info(`MCP server.registerTool completed for: ${tool.name}`);
    } catch (error) {
      logger.error(`Failed to register tool ${tool.name} with MCP server`, {
        error,
        errorMessage: error instanceof Error ? error.message : String(error),
        errorStack: error instanceof Error ? error.stack : undefined,
      });
      throw error;
    }

    logger.info(`Tool ${tool.name} registered successfully`);
  }

  /**
   * Registers multiple tools at once
   * @param tools - Array of tool definitions
   */
  registerTools(tools: ToolDefinition[]): void {
    for (const tool of tools) {
      this.registerTool(tool);
    }
  }

  /**
   * Gets a registered tool by name
   * @param name - Tool name
   * @returns The tool definition or undefined
   */
  getTool(name: string): ToolDefinition | undefined {
    return this.tools.get(name);
  }

  /**
   * Gets all registered tools
   * @returns Array of tool names
   */
  getToolNames(): string[] {
    return Array.from(this.tools.keys());
  }

  /**
   * Checks if a tool is registered
   * @param name - Tool name
   * @returns True if registered
   */
  hasTool(name: string): boolean {
    return this.tools.has(name);
  }

  /**
   * Updates the default dependencies
   * @param deps - New default dependencies
   */
  updateDefaultDeps(deps: BaseHandlerDeps): void {
    this.defaultDeps = deps;
    logger.debug('Default dependencies updated');
  }

  /**
   * Discovers and loads tools from specified directories (requires FEATURE_TOOL_DISCOVERY)
   * @param options - Discovery options
   * @returns Array of discovered tool names
   */
  async discoverTools(options: ToolDiscoveryOptions = {}): Promise<string[]> {
    if (!isFeatureEnabled('toolDiscovery')) {
      logger.debug('Tool discovery is disabled by feature flag');
      return [];
    }

    const {
      directories = ['./tools'],
      patterns = ['*.tool.js', '*.tool.mjs'],
      recursive = true,
      includeCategories,
      excludeCategories,
      includeTags,
      excludeTags,
    } = options;

    logger.info('Starting tool discovery', { directories, patterns });
    const discoveredTools: string[] = [];

    for (const directory of directories) {
      try {
        const filterOptions: {
          includeCategories?: string[];
          excludeCategories?: string[];
          includeTags?: string[];
          excludeTags?: string[];
        } = {};

        if (includeCategories) filterOptions.includeCategories = includeCategories;
        if (excludeCategories) filterOptions.excludeCategories = excludeCategories;
        if (includeTags) filterOptions.includeTags = includeTags;
        if (excludeTags) filterOptions.excludeTags = excludeTags;

        const toolsFound = await this.scanDirectory(directory, patterns, recursive, filterOptions);
        discoveredTools.push(...toolsFound);
      } catch (error) {
        logger.warn(`Failed to scan directory: ${directory}`, error);
      }
    }

    logger.info(`Tool discovery complete. Found ${discoveredTools.length} tools`);
    return discoveredTools;
  }

  /**
   * Scans a directory for tool files (internal)
   */
  private async scanDirectory(
    directory: string,
    patterns: string[],
    recursive: boolean,
    filters: {
      includeCategories?: string[];
      excludeCategories?: string[];
      includeTags?: string[];
      excludeTags?: string[];
    }
  ): Promise<string[]> {
    const discoveredTools: string[] = [];

    try {
      const entries = await fs.readdir(directory, { withFileTypes: true });

      for (const entry of entries) {
        const fullPath = join(directory, entry.name);

        if (entry.isDirectory() && recursive) {
          const subTools = await this.scanDirectory(fullPath, patterns, recursive, filters);
          discoveredTools.push(...subTools);
        } else if (entry.isFile() && ToolRegistry.matchesPattern(entry.name, patterns)) {
          const toolName = await this.loadToolFromFile(fullPath, filters);
          if (toolName) {
            discoveredTools.push(toolName);
            this.discoveredTools.set(toolName, fullPath);
          }
        }
      }
    } catch (error) {
      logger.error(`Failed to scan directory: ${directory}`, error);
    }

    return discoveredTools;
  }

  /**
   * Checks if a filename matches any of the patterns
   */
  private static matchesPattern(filename: string, patterns: string[]): boolean {
    return patterns.some((pattern) => {
      // Convert glob pattern to regex
      // Escape special regex characters except for *
      const escaped = pattern.replace(/[-[\]{}()+?.,\\^$|#\s]/g, '\\$&');
      // Replace * with [^/]* (matches any sequence except path separator)
      // Anchor the pattern to match the entire filename
      const regexStr = `^${escaped.replace(/\*/g, '[^/]*')}$`;
      const regex = new RegExp(regexStr);
      return regex.test(filename);
    });
  }

  /**
   * Loads a tool from a file
   */
  private async loadToolFromFile(
    filePath: string,
    filters: {
      includeCategories?: string[];
      excludeCategories?: string[];
      includeTags?: string[];
      excludeTags?: string[];
    }
  ): Promise<string | null> {
    try {
      // Security validation: ensure the file path is within expected directories
      const normalizedPath = normalize(filePath);
      const resolvedPath = resolve(normalizedPath);

      // Check for path traversal attempts
      if (normalizedPath.includes('..') || !resolvedPath.startsWith(process.cwd())) {
        logger.error(`Security: Rejected file path outside project directory: ${filePath}`);
        return null;
      }

      // Additional validation: ensure it's a JavaScript/TypeScript file
      if (!/\.(js|mjs|ts)$/.test(normalizedPath)) {
        logger.error(`Security: Rejected non-JavaScript file: ${filePath}`);
        return null;
      }

      logger.debug(`Loading tool from file: ${filePath}`);
      const module = (await import(filePath)) as Record<string, unknown>;

      let toolDef: ToolDefinition | null = null;

      if (module.toolDefinition && typeof module.toolDefinition === 'object') {
        toolDef = module.toolDefinition as ToolDefinition;
      } else if (module.default && typeof module.default === 'object') {
        const defaultExport = module.default as Record<string, unknown>;
        if (
          'name' in defaultExport &&
          'handler' in defaultExport &&
          'description' in defaultExport
        ) {
          toolDef = defaultExport as unknown as ToolDefinition;
        }
      }

      if (!toolDef) {
        logger.warn(`No valid tool definition found in: ${filePath}`);
        return null;
      }

      if (!ToolRegistry.passesFilters(toolDef, filters)) {
        logger.debug(`Tool ${toolDef.name} filtered out`);
        return null;
      }

      if (toolDef.metadata?.enabled === false) {
        logger.info(`Tool ${toolDef.name} is disabled, skipping registration`);
        return null;
      }

      this.registerTool(toolDef);
      logger.info(`Successfully loaded tool: ${toolDef.name} from ${filePath}`);
      return toolDef.name;
    } catch (error) {
      logger.error(`Failed to load tool from file: ${filePath}`, error);
      return null;
    }
  }

  /**
   * Checks if a tool passes the configured filters
   */
  private static passesFilters(
    tool: ToolDefinition,
    filters: {
      includeCategories?: string[];
      excludeCategories?: string[];
      includeTags?: string[];
      excludeTags?: string[];
    }
  ): boolean {
    const metadata = tool.metadata || {};

    if (filters.includeCategories?.length) {
      if (!metadata.category || !filters.includeCategories.includes(metadata.category)) {
        return false;
      }
    }

    if (filters.excludeCategories?.length) {
      if (metadata.category && filters.excludeCategories.includes(metadata.category)) {
        return false;
      }
    }

    if (filters.includeTags?.length) {
      if (!metadata.tags || !metadata.tags.some((tag) => filters.includeTags?.includes(tag))) {
        return false;
      }
    }

    if (filters.excludeTags?.length) {
      if (metadata.tags?.some((tag) => filters.excludeTags?.includes(tag))) {
        return false;
      }
    }

    return true;
  }

  /**
   * Gets tool metadata
   * @param name - Tool name
   * @returns Tool metadata or undefined
   */
  getToolMetadata(name: string): ToolMetadata | undefined {
    return this.toolMetadata.get(name);
  }

  /**
   * Gets tools by category (requires metadata)
   * @param category - Category to filter by
   * @returns Array of tool names in the category
   */
  getToolsByCategory(category: string): string[] {
    const tools: string[] = [];
    for (const [name, metadata] of this.toolMetadata.entries()) {
      if (metadata.category === category) {
        tools.push(name);
      }
    }
    return tools;
  }

  /**
   * Gets tools by tag (requires metadata)
   * @param tag - Tag to filter by
   * @returns Array of tool names with the tag
   */
  getToolsByTag(tag: string): string[] {
    const tools: string[] = [];
    for (const [name, metadata] of this.toolMetadata.entries()) {
      if (metadata.tags?.includes(tag)) {
        tools.push(name);
      }
    }
    return tools;
  }

  /**
   * Gets all tool categories
   * @returns Array of unique categories
   */
  getCategories(): string[] {
    const categories = new Set<string>();
    for (const metadata of this.toolMetadata.values()) {
      if (metadata.category) {
        categories.add(metadata.category);
      }
    }
    return Array.from(categories);
  }

  /**
   * Gets all tool tags
   * @returns Array of unique tags
   */
  getTags(): string[] {
    const tags = new Set<string>();
    for (const metadata of this.toolMetadata.values()) {
      if (metadata.tags) {
        metadata.tags.forEach((tag) => tags.add(tag));
      }
    }
    return Array.from(tags);
  }

  /**
   * Gets enhanced tool information including metadata
   * @returns Array of tool information objects
   */
  getToolsInfo(): Array<{
    name: string;
    description: string;
    category?: string;
    version?: string;
    tags?: string[];
    enabled?: boolean;
    discovered?: boolean;
  }> {
    const toolsInfo = [];

    for (const name of this.getToolNames()) {
      const tool = this.getTool(name);
      const metadata = this.getToolMetadata(name);
      const discovered = this.discoveredTools.has(name);

      if (tool) {
        toolsInfo.push({
          name: tool.name,
          description: tool.description,
          ...(metadata?.category && { category: metadata.category }),
          ...(metadata?.version && { version: metadata.version }),
          ...(metadata?.tags && { tags: metadata.tags }),
          enabled: metadata?.enabled !== false,
          discovered,
        });
      }
    }

    return toolsInfo;
  }
}
