#!/usr/bin/env node

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { promptFunctions } from "./prompts.js";
import {
  loadConfigSync,
  loadPresetConfigs,
  listAvailablePresets,
  mergeConfigs,
  validateToolConfig,
  convertParametersToJsonSchema,
  convertParametersToZodSchema,
} from "./config.js";
import { fileURLToPath } from "url";
import { dirname, join } from "path";
import fs from "fs";

// Define TypeScript interfaces for returned types
interface CommandLineArgs {
  configPath?: string;
  presets: string[];
}

/**
 * Gets the package version from package.json
 * @returns {Object} The parsed package.json content
 */
function getPackageInfo(): Record<string, any> {
  try {
    const __filename = fileURLToPath(import.meta.url);
    const __dirname = dirname(__filename);
    const packageJsonPath = join(__dirname, "..", "package.json");

    // Check if file exists before reading
    if (!fs.existsSync(packageJsonPath)) {
      console.error(`Package.json not found at ${packageJsonPath}`);
      return { version: "0.1.7" }; // Fallback to hardcoded version
    }

    try {
      return JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
    } catch (parseError) {
      console.error(`Error parsing package.json: ${parseError}`);
      return { version: "0.1.7" }; // Fallback to hardcoded version
    }
  } catch (error) {
    console.error(`Error accessing package info: ${error}`);
    return { version: "0.1.7" }; // Fallback to hardcoded version
  }
}

/**
 * Parses command line arguments to extract config path and presets
 * @returns {Object} Object containing configPath and presets
 */
function parseCommandLineArgs(): CommandLineArgs {
  const args = process.argv.slice(2);
  let configPath: string | undefined;
  let presets: string[] = []; // Initialize with empty array
  let hasPreset = false;

  for (let i = 0; i < args.length; i++) {
    if (args[i] === "--config" && i + 1 < args.length) {
      configPath = args[i + 1];
    } else if (args[i] === "--preset" && i + 1 < args.length) {
      // Split comma-separated presets
      presets = args[i + 1].split(",");
      hasPreset = true;
    }
  }

  // Only default to thinking preset if no preset specified AND no config path provided
  if (presets.length === 0 && !configPath) {
    presets = ["thinking"];
  }

  return { configPath, presets };
}

/**
 * Loads and merges configuration from presets and user config
 * @param presets - Array of preset names to load
 * @param configPath - Optional path to user config
 */
function loadAndMergeConfig(
  presets: string[],
  configPath?: string
): Record<string, any> {
  // Log available presets
  const availablePresets = listAvailablePresets();
  console.error(`Available presets: ${availablePresets.join(", ")}`);
  console.error(`Using presets: ${presets.join(", ")}`);

  // 1. Load preset configs
  const presetConfig = loadPresetConfigs(presets);
  console.error(
    `Loaded ${Object.keys(presetConfig).length} tools from presets`
  );

  // 2. Load user configs from .workflows directory if provided
  const userConfig = configPath ? loadConfigSync(configPath) : {};
  if (configPath) {
    console.error(
      `Loaded ${
        Object.keys(userConfig).length
      } tool configurations from user config directory: ${configPath}`
    );
  }

  // 3. Merge configs (user config overrides preset config)
  const finalConfig = mergeConfigs(presetConfig, userConfig);
  console.error(
    `Final configuration contains ${Object.keys(finalConfig).length} tools`
  );

  return finalConfig;
}

/**
 * Creates and configures the MCP server with tools from the provided configuration
 * @param config - The tool configuration object
 * @param version - Server version
 */
function createMcpServer(
  config: Record<string, any>,
  version: string
): McpServer {
  // Create an MCP server
  const server = new McpServer(
    {
      name: "DevTools MCP",
      version: version,
    },
    {
      capabilities: {
        tools: {},
      },
    }
  );

  // Register all tools from the config
  registerToolsFromConfig(server, config);

  // If no tools were registered, add a dummy tool
  if (
    Object.keys(config).filter((key) => !config[key]?.disabled).length === 0
  ) {
    console.error("No tools registered, adding placeholder tool");
    server.tool(
      "placeholder",
      "This is a placeholder tool when no other tools are loaded",
      async () => {
        return {
          content: [
            {
              type: "text",
              text: "No tools are currently configured",
            },
          ],
        };
      }
    );
  }

  return server;
}

/**
 * Registers tools to the MCP server based on the provided configuration
 * @param server - The MCP server instance
 * @param config - The tool configuration object
 */
function registerToolsFromConfig(
  server: McpServer,
  config: Record<string, any>
): void {
  // Function to add a tool if it's not disabled
  const addTool = (
    name: string,
    description: string,
    inputSchema: any | undefined,
    callback: (params?: Record<string, any>) => Promise<any>
  ) => {
    server.tool(
      name,
      description,
      inputSchema,
      async (params: Record<string, any>) => {
        try {
          // Log parameters for debugging
          console.error(`Tool ${name} called with params:`, params);

          // Validate tool configuration
          const validationError = validateToolConfig(config, name);
          if (validationError) {
            return {
              content: [{ type: "text", text: `Error: ${validationError}` }],
              isError: true,
            };
          }

          // Pass the params to the callback
          return callback(params);
        } catch (error) {
          // Improved error handling
          const errorMessage =
            error instanceof Error ? error.message : String(error);
          console.error(`Error executing tool ${name}:`, errorMessage);
          return {
            content: [
              {
                type: "text",
                text: `Error executing tool: ${errorMessage}`,
              },
            ],
            isError: true,
          };
        }
      }
    );
  };

  // Dynamically register all tools from the config
  Object.entries(config).forEach(([key, toolConfig]: [string, any]) => {
    // Skip if toolConfig is undefined or disabled
    if (!toolConfig || toolConfig.disabled) {
      return;
    }

    // Find corresponding prompt function if available
    const promptFunction = promptFunctions[key];

    // Use custom name if provided, otherwise use the key
    const toolName = toolConfig.name || key;

    // Convert parameters to Zod schema if provided
    let inputSchema = undefined;
    if (
      toolConfig.parameters &&
      Object.keys(toolConfig.parameters).length > 0
    ) {
      // Use Zod schema instead of JSON Schema for better MCP SDK compatibility
      inputSchema = convertParametersToZodSchema(toolConfig.parameters);
      // Log the input schema for debugging
      console.error(
        `Tool ${toolName} input schema:`,
        JSON.stringify(Object.keys(inputSchema), null, 2)
      );
    }

    addTool(
      toolName,
      toolConfig.description || `${key.replace(/_/g, " ")} tool`,
      inputSchema,
      async (params?: Record<string, any>) => {
        // Generate the tool prompt
        let text = generateToolPrompt(key, toolConfig, promptFunction, config);

        // If we have parameters, add them to the response
        if (params && Object.keys(params).length > 0) {
          text += `\n\nParameters: ${JSON.stringify(params, null, 2)}`;
        }

        return { content: [{ type: "text", text }] };
      }
    );
  });
}

/**
 * Generates the prompt text for a tool based on its configuration
 * @param key - The tool key
 * @param toolConfig - The tool configuration
 * @param promptFunction - Optional prompt generation function
 * @param fullConfig - The complete configuration object
 */
function generateToolPrompt(
  key: string,
  toolConfig: Record<string, any>,
  promptFunction: ((config: Record<string, any>) => string) | undefined,
  fullConfig: Record<string, any>
): string {
  // Use the prompt function if available
  if (promptFunction) {
    return promptFunction(fullConfig);
  }

  // Otherwise use the prompt directly from config
  if (toolConfig.prompt) {
    let text = toolConfig.prompt;

    // Add context if provided
    if (toolConfig.context) {
      text += `\n\n${toolConfig.context}`;
    }

    // Add tools section if provided
    if (toolConfig.tools && toolConfig.tools.length > 0) {
      text += "\n\n## Available Tools\n";

      if (toolConfig.toolMode === "sequential") {
        text +=
          "If all required user input/feedback is acquired or if no input/feedback is needed, execute this exact sequence of tools to complete this task:\n\n";

        toolConfig.tools.forEach((tool: any, index: number) => {
          text += `${index + 1}. **${tool.name}**`;
          if (tool.description) {
            text += `: ${tool.description}`;
          }
          text += "\n";
        });
      } else {
        // Default to dynamic mode
        text += `Use these tools as needed to complete the user's request:\n\n`;

        toolConfig.tools.forEach((tool: any) => {
          text += `- **${tool.name}**`;
          if (tool.description) {
            text += `: ${tool.description}`;
          }
          text += "\n";
        });
      }
    }

    return text;
  }

  // No prompt available
  console.error(`Tool "${key}" has no prompt defined`);
  return `# ${key}\n\nNo prompt defined for this tool.`;
}

/**
 * Starts the MCP server with the specified transport
 * @param server - The configured MCP server
 * @param presets - Array of preset names used
 * @param configPath - Optional path to user config
 */
async function startServer(
  server: McpServer,
  presets: string[],
  configPath?: string
): Promise<void> {
  const transport = new StdioServerTransport();

  // Critical: Make sure stdin/stdout are in raw mode
  // This prevents the shell from interpreting JSON as commands
  if (process.stdin.isTTY) {
    process.stdin.setRawMode(true);
  }

  // Ensure stdio is properly set up to handle binary data
  process.stdin.setEncoding("utf8");

  // Completely replace stdout.write to ensure strict JSON-RPC handling
  const originalStdoutWrite = process.stdout.write.bind(process.stdout);
  process.stdout.write = (chunk: any, encoding?: any, callback?: any) => {
    try {
      // Only process valid JSON objects
      if (typeof chunk === "string" && chunk.trim().startsWith("{")) {
        // Validate JSON format before sending
        JSON.parse(chunk.trim());
        return originalStdoutWrite(chunk, encoding, callback);
      }

      // Log non-JSON to stderr
      if (typeof chunk === "string" && chunk.trim()) {
        console.error(
          `[stdout filtered]: ${chunk.substring(0, 100)}${
            chunk.length > 100 ? "..." : ""
          }`
        );
      }

      // Indicate success without writing anything
      if (callback) callback();
      return true;
    } catch (e) {
      // Log JSON parsing errors to stderr
      console.error(
        `[JSON error]: ${e instanceof Error ? e.message : String(e)}`
      );
      if (callback) callback();
      return true;
    }
  };

  try {
    // Connect to the MCP server
    await server.connect(transport);

    // Log successful connection
    console.error(
      `DevTools MCP server running with presets: ${presets.join(", ")}${
        configPath ? ` and user config from: ${configPath}` : ""
      }`
    );

    // Keep the process alive
    process.on("SIGINT", () => {
      console.error("MCP server shutting down...");
      process.exit(0);
    });
  } catch (err) {
    console.error("Error starting server:", err);
    process.exit(1);
  }
}

// Main execution
(async function main() {
  try {
    // Use hardcoded version to avoid package.json issues with npx
    const version = "0.1.7"; // Match the current package version

    // Parse command line arguments
    const { configPath, presets } = parseCommandLineArgs();

    // Log startup information
    console.error(
      `Starting MCP server v${version} with presets: ${presets.join(", ")}`
    );

    // Load and merge configuration - wrap in try/catch for better error handling
    let finalConfig;
    try {
      finalConfig = loadAndMergeConfig(presets, configPath);
    } catch (error) {
      console.error("Error loading configuration:", error);
      process.exit(1);
    }

    // Create and configure the MCP server
    const server = createMcpServer(finalConfig, version);

    // Start the server
    await startServer(server, presets, configPath);

    // Keep process alive for incoming connections
    process.stdin.on("end", () => {
      console.error("Input stream ended, shutting down...");
      process.exit(0);
    });

    process.on("uncaughtException", (err) => {
      console.error("Uncaught exception:", err);
      process.exit(1);
    });
  } catch (err) {
    console.error("Fatal error:", err);
    process.exit(1);
  }
})();
