/**
 * Railway Dynamic Tool Executor (Deno)
 * Uses Deno's native HTTP import support
 */

// Import zod-to-json-schema for Zod v3 support
import { zodToJsonSchema } from 'https://esm.sh/zod-to-json-schema@3.25.0';

// Cache for imported tool modules
// biome-ignore lint/suspicious/noExplicitAny: Tool types are dynamic and vary by package
const moduleCache = new Map<string, any>();

// Web app API URL for health status reporting
const TPMJS_API_URL = Deno.env.get('TPMJS_API_URL') || 'https://tpmjs.com';

/**
 * Report tool execution result to centralized health service
 * Non-blocking - fires and forgets to avoid slowing down execution
 */
async function reportToolHealth(
  packageName: string,
  exportName: string,
  success: boolean,
  error?: string
): Promise<void> {
  try {
    const response = await fetch(`${TPMJS_API_URL}/api/tools/report-health`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        packageName,
        exportName,
        success,
        error,
      }),
    });

    if (response.ok) {
      console.log(
        `📊 Health reported for ${packageName}/${exportName}: ${success ? 'SUCCESS' : 'FAILURE'}`
      );
    } else {
      console.warn(`⚠️  Failed to report health: ${response.status}`);
    }
  } catch (err) {
    // Non-blocking - just log
    console.error('❌ Failed to report tool health:', err);
  }
}

/**
 * Sanitize JSON Schema to fix common issues
 * - Replaces invalid type "None" with "object"
 * - Ensures type is always set
 * - Ensures object schemas have properties
 */
// biome-ignore lint/suspicious/noExplicitAny: JSON Schema can have any structure
function sanitizeJsonSchema(schema: any): any {
  if (!schema || typeof schema !== 'object') {
    console.warn('⚠️  Invalid schema (not an object), returning default object schema');
    return { type: 'object', properties: {}, additionalProperties: false };
  }

  // Clone the schema to avoid mutating the original
  const sanitized = { ...schema };

  // Fix invalid type "None" (common in Python-based tools)
  if (sanitized.type === 'None' || sanitized.type === 'none' || sanitized.type === null) {
    console.warn(`⚠️  Invalid schema type "${sanitized.type}", replacing with "object"`);
    sanitized.type = 'object';
    if (!sanitized.properties) {
      sanitized.properties = {};
    }
    if (sanitized.additionalProperties === undefined) {
      sanitized.additionalProperties = false;
    }
  }

  // Ensure type is set
  if (!sanitized.type) {
    console.warn('⚠️  Schema missing type, defaulting to "object"');
    sanitized.type = 'object';
    if (!sanitized.properties) {
      sanitized.properties = {};
    }
    if (sanitized.additionalProperties === undefined) {
      sanitized.additionalProperties = false;
    }
  }

  // Recursively sanitize nested schemas
  if (sanitized.properties && typeof sanitized.properties === 'object') {
    for (const [key, value] of Object.entries(sanitized.properties)) {
      if (value && typeof value === 'object') {
        sanitized.properties[key] = sanitizeJsonSchema(value);
      }
    }
  }

  // Sanitize array items
  if (sanitized.items && typeof sanitized.items === 'object') {
    sanitized.items = sanitizeJsonSchema(sanitized.items);
  }

  // Sanitize anyOf/oneOf/allOf
  for (const key of ['anyOf', 'oneOf', 'allOf']) {
    if (Array.isArray(sanitized[key])) {
      sanitized[key] = sanitized[key].map((s: any) => sanitizeJsonSchema(s));
    }
  }

  return sanitized;
}

/**
 * Load and describe a tool from esm.sh
 */
async function loadAndDescribe(req: Request): Promise<Response> {
  try {
    const body = await req.json();
    const { packageName, exportName, version, importUrl, env } = body;

    if (!packageName || !exportName || !version) {
      return Response.json(
        {
          success: false,
          error: 'Missing required fields: packageName, exportName, version',
        },
        { status: 400 }
      );
    }

    const cacheKey = `${packageName}::${exportName}`;

    // biome-ignore lint/suspicious/noImplicitAnyLet: Tool type is determined dynamically after import
    let toolModule;

    // Check cache first
    if (moduleCache.has(cacheKey)) {
      console.log(`✅ Cache hit: ${cacheKey}`);
      toolModule = moduleCache.get(cacheKey);
    } else {
      // Dynamic import from esm.sh (Deno supports this natively!)
      const url = importUrl || `https://esm.sh/${packageName}@${version}`;
      console.log(`📦 Importing: ${url}`);

      const module = await import(url);
      let rawExport = module[exportName];

      if (!rawExport) {
        console.error(`❌ Export "${exportName}" not found. Available:`, Object.keys(module));
        return Response.json(
          {
            success: false,
            error: `Export "${exportName}" not found in module`,
            availableExports: Object.keys(module),
          },
          { status: 404 }
        );
      }

      // Check if it's a factory function (not a direct tool)
      if (typeof rawExport === 'function' && !rawExport.description && !rawExport.execute) {
        console.log(`🏭 Detected factory function for ${cacheKey}, attempting to call...`);

        let factoryResult = null;

        // Strategy 1: Try calling with no arguments
        try {
          console.log(`  Trying: ${exportName}()`);
          factoryResult = rawExport();
          if (factoryResult?.description && factoryResult?.execute) {
            console.log('  ✅ Success with no-args factory');
            rawExport = factoryResult;
          }
        } catch (error) {
          console.log('  ❌ No-args failed:', error.message);
        }

        // Strategy 2: Try calling with env vars as config object
        if (!factoryResult && env && typeof env === 'object') {
          // Build multiple config variations to try
          const configVariations = [];

          // Variation 1: Raw env vars (e.g., { VALYU_API_KEY: 'xxx' })
          configVariations.push({ ...env });

          // Variation 2: Normalized to camelCase apiKey (e.g., { apiKey: 'xxx' })
          const apiKeyValue = Object.entries(env).find(([key]) =>
            key.toUpperCase().includes('API_KEY')
          )?.[1];
          if (apiKeyValue) {
            configVariations.push({ apiKey: apiKeyValue });
          }

          // Variation 3: Normalized to key (e.g., { key: 'xxx' })
          if (apiKeyValue) {
            configVariations.push({ key: apiKeyValue });
          }

          // Try each config variation
          for (const config of configVariations) {
            try {
              console.log(`  Trying: ${exportName}(`, Object.keys(config), ')');
              factoryResult = rawExport(config);
              if (factoryResult?.description && factoryResult?.execute) {
                console.log('  ✅ Success with config:', Object.keys(config));
                rawExport = factoryResult;
                break;
              }
            } catch (error) {
              console.log('  ❌ Config', Object.keys(config), 'failed:', error.message);
            }
          }
        }

        // Strategy 3: Try calling with first env var value (single-arg pattern)
        if (!factoryResult && env && typeof env === 'object') {
          try {
            const firstValue = Object.values(env)[0];
            if (firstValue) {
              console.log(`  Trying: ${exportName}(firstEnvValue)`);
              factoryResult = rawExport(firstValue);
              if (factoryResult?.description && factoryResult?.execute) {
                console.log('  ✅ Success with single-arg factory');
                rawExport = factoryResult;
              }
            }
          } catch (error) {
            console.log('  ❌ Single-arg failed:', error.message);
          }
        }

        // If all factory strategies failed, return error
        if (!factoryResult) {
          console.error('❌ Factory function detected but all call strategies failed');
          return Response.json(
            {
              success: false,
              error: `Tool "${exportName}" is a factory function but couldn't be initialized. Tried: no-args, config object, and single-arg patterns.`,
              hint: 'This tool may require specific configuration. Check package documentation.',
            },
            { status: 400 }
          );
        }
      }

      toolModule = rawExport;

      // Validate it's an AI SDK tool
      if (!toolModule.description || !toolModule.execute) {
        console.error('❌ Invalid AI SDK tool structure:', {
          hasDescription: !!toolModule.description,
          hasExecute: !!toolModule.execute,
          hasInputSchema: !!toolModule.inputSchema,
          keys: Object.keys(toolModule),
        });
        return Response.json(
          {
            success: false,
            error: 'Invalid AI SDK tool structure (missing description or execute)',
            toolKeys: Object.keys(toolModule),
          },
          { status: 400 }
        );
      }

      // Cache it
      moduleCache.set(cacheKey, toolModule);
      console.log(`✅ Cached: ${cacheKey}`);
    }

    // Extract tool definition - try multiple schema formats
    let rawJsonSchema = null;

    if (toolModule.inputSchema) {
      // Strategy 1: Try Zod v4 native JSON Schema export
      if (typeof toolModule.inputSchema.toJSONSchema === 'function') {
        console.log(`📋 Using Zod v4 toJSONSchema() for ${cacheKey}`);
        try {
          rawJsonSchema = toolModule.inputSchema.toJSONSchema();
        } catch (error) {
          console.warn(`⚠️  Zod toJSONSchema() failed for ${cacheKey}:`, error);
        }
      } else if (typeof toolModule.inputSchema.jsonSchema === 'function') {
        console.log(`📋 Using Zod v4 jsonSchema() for ${cacheKey}`);
        try {
          rawJsonSchema = toolModule.inputSchema.jsonSchema();
        } catch (error) {
          console.warn(`⚠️  Zod jsonSchema() failed for ${cacheKey}:`, error);
        }
      }

      // Strategy 2: Try AI SDK v6 jsonSchema() wrapper (has .schema property)
      if (!rawJsonSchema && toolModule.inputSchema.schema) {
        console.log(`📋 Using AI SDK jsonSchema.schema for ${cacheKey}`);
        rawJsonSchema = toolModule.inputSchema.schema;
      }

      // Strategy 2.5: Try AI SDK jsonSchema() wrapper (has .jsonSchema property)
      // Note: Some versions use .jsonSchema instead of .schema
      if (
        !rawJsonSchema &&
        toolModule.inputSchema.jsonSchema &&
        typeof toolModule.inputSchema.jsonSchema === 'object'
      ) {
        console.log(`📋 Using AI SDK jsonSchema.jsonSchema for ${cacheKey}`);
        rawJsonSchema = toolModule.inputSchema.jsonSchema;
      }

      // Strategy 3: Try Zod v3 schema (detect via _def property and convert)
      if (!rawJsonSchema && toolModule.inputSchema._def) {
        console.log(
          `📋 Detected Zod schema (v3), converting with zod-to-json-schema for ${cacheKey}`
        );
        try {
          rawJsonSchema = zodToJsonSchema(toolModule.inputSchema);
          console.log(`✅ Successfully converted Zod schema for ${cacheKey}`);
        } catch (error) {
          console.warn(`⚠️  zod-to-json-schema conversion failed for ${cacheKey}:`, error);
        }
      }
    }

    // If no schema found, fail with helpful error
    if (!rawJsonSchema) {
      console.error(`❌ No valid schema found for ${cacheKey}`, {
        hasInputSchema: !!toolModule.inputSchema,
        inputSchemaType: typeof toolModule.inputSchema,
        hasToJSONSchema: typeof toolModule.inputSchema?.toJSONSchema === 'function',
        hasJsonSchemaFunction: typeof toolModule.inputSchema?.jsonSchema === 'function',
        hasJsonSchemaProperty:
          !!toolModule.inputSchema?.jsonSchema &&
          typeof toolModule.inputSchema?.jsonSchema === 'object',
        hasSchema: !!toolModule.inputSchema?.schema,
        keys: toolModule.inputSchema ? Object.keys(toolModule.inputSchema) : [],
      });
      return Response.json(
        {
          success: false,
          error: `Tool "${exportName}" has no valid inputSchema. Tools must use AI SDK jsonSchema(), Zod v4 toJSONSchema(), or Zod v3 schemas.`,
          debug: {
            hasInputSchema: !!toolModule.inputSchema,
            availableMethods: toolModule.inputSchema ? Object.keys(toolModule.inputSchema) : [],
            hasZodDef: !!toolModule.inputSchema?._def,
          },
        },
        { status: 400 }
      );
    }

    console.log(`✅ Extracted schema for ${cacheKey}`);

    // Sanitize schema - fix common issues with invalid schemas
    const sanitizedSchema = sanitizeJsonSchema(rawJsonSchema);

    return Response.json({
      success: true,
      tool: {
        exportName,
        description: toolModule.description,
        inputSchema: sanitizedSchema, // Plain JSON Schema - fully serializable
      },
    });
  } catch (error) {
    console.error('❌ Failed to load tool:', error);
    return Response.json(
      {
        success: false,
        error: error.message,
      },
      { status: 500 }
    );
  }
}

/**
 * Execute a tool with parameters
 */
async function executeTool(req: Request): Promise<Response> {
  const startTime = Date.now();
  // Declare these before try block so they're available in catch for error reporting
  let packageName = 'unknown';
  let exportName = 'unknown';
  try {
    const body = await req.json();
    const { packageName: pkg, exportName: exp, version, importUrl, params, env } = body;
    packageName = pkg || 'unknown';
    exportName = exp || 'unknown';

    console.log('📥 Execute request:', {
      packageName,
      exportName,
      version,
      envKeys: env ? Object.keys(env) : [],
      envValues: env || {},
    });

    if (!packageName || !exportName || !version) {
      return Response.json(
        {
          success: false,
          error: 'Missing required fields: packageName, exportName, version',
        },
        { status: 400 }
      );
    }

    const cacheKey = `${packageName}::${exportName}`;

    // biome-ignore lint/suspicious/noImplicitAnyLet: Tool type is determined dynamically after import
    let toolModule;

    // Check cache or import
    if (moduleCache.has(cacheKey)) {
      console.log(`✅ Using cached tool: ${cacheKey}`);
      toolModule = moduleCache.get(cacheKey);
    } else {
      const url = importUrl || `https://esm.sh/${packageName}@${version}`;
      console.log(`📦 Importing for execution: ${url}`);

      const module = await import(url);
      let rawExport = module[exportName];

      if (!rawExport) {
        return Response.json(
          {
            success: false,
            error: 'Tool not found',
            executionTimeMs: Date.now() - startTime,
          },
          { status: 404 }
        );
      }

      // Check if it's a factory function (not a direct tool)
      if (typeof rawExport === 'function' && !rawExport.description && !rawExport.execute) {
        console.log(`🏭 Detected factory function for ${cacheKey}, attempting to call...`);

        let factoryResult = null;

        // Strategy 1: Try calling with no arguments
        try {
          console.log(`  Trying: ${exportName}()`);
          factoryResult = rawExport();
          if (factoryResult?.execute) {
            console.log('  ✅ Success with no-args factory');
            rawExport = factoryResult;
          }
        } catch (error) {
          console.log('  ❌ No-args failed:', error.message);
        }

        // Strategy 2: Try calling with env vars as config object
        if (!factoryResult && env && typeof env === 'object') {
          // Build multiple config variations to try
          const configVariations = [];

          // Variation 1: Raw env vars (e.g., { VALYU_API_KEY: 'xxx' })
          configVariations.push({ ...env });

          // Variation 2: Normalized to camelCase apiKey (e.g., { apiKey: 'xxx' })
          const apiKeyValue = Object.entries(env).find(([key]) =>
            key.toUpperCase().includes('API_KEY')
          )?.[1];
          if (apiKeyValue) {
            configVariations.push({ apiKey: apiKeyValue });
          }

          // Variation 3: Normalized to key (e.g., { key: 'xxx' })
          if (apiKeyValue) {
            configVariations.push({ key: apiKeyValue });
          }

          // Try each config variation
          for (const config of configVariations) {
            try {
              console.log(`  Trying: ${exportName}(`, Object.keys(config), ')');
              factoryResult = rawExport(config);
              if (factoryResult?.execute) {
                console.log('  ✅ Success with config:', Object.keys(config));
                rawExport = factoryResult;
                break;
              }
            } catch (error) {
              console.log('  ❌ Config', Object.keys(config), 'failed:', error.message);
            }
          }
        }

        // Strategy 3: Try calling with first env var value (single-arg pattern)
        if (!factoryResult && env && typeof env === 'object') {
          try {
            const firstValue = Object.values(env)[0];
            if (firstValue) {
              console.log(`  Trying: ${exportName}(firstEnvValue)`);
              factoryResult = rawExport(firstValue);
              if (factoryResult?.execute) {
                console.log('  ✅ Success with single-arg factory');
                rawExport = factoryResult;
              }
            }
          } catch (error) {
            console.log('  ❌ Single-arg failed:', error.message);
          }
        }

        if (!factoryResult) {
          return Response.json(
            {
              success: false,
              error: `Tool "${exportName}" is a factory function but couldn't be initialized`,
              executionTimeMs: Date.now() - startTime,
            },
            { status: 400 }
          );
        }
      }

      toolModule = rawExport;

      if (!toolModule.execute) {
        return Response.json(
          {
            success: false,
            error: 'Tool missing execute function',
            executionTimeMs: Date.now() - startTime,
          },
          { status: 400 }
        );
      }

      moduleCache.set(cacheKey, toolModule);
    }

    // Inject environment variables from client
    if (env && typeof env === 'object') {
      const envKeys = Object.keys(env);
      if (envKeys.length > 0) {
        console.log(`🔐 Injecting ${envKeys.length} environment variables:`, envKeys);
        for (const [key, value] of Object.entries(env)) {
          const stringValue = String(value);

          // Set in Deno environment (for esm.sh imports)
          Deno.env.set(key, stringValue);

          // ALSO set in Node.js process.env (for npm: imports)
          // @ts-ignore - process is available in Node.js compatibility mode
          if (typeof globalThis.process !== 'undefined' && globalThis.process.env) {
            // @ts-ignore - process.env exists in Node compat mode
            globalThis.process.env[key] = stringValue;
          }

          console.log(`  ✅ Set ${key} = ${stringValue.substring(0, 10)}...`);
        }
        // Verify they're set in both places
        console.log(
          '🔍 Verification - Deno.env has:',
          envKeys.map((k) => `${k}=${Deno.env.get(k)?.substring(0, 10)}...`)
        );
        // @ts-ignore - process is available in Node.js compatibility mode
        if (typeof globalThis.process !== 'undefined' && globalThis.process.env) {
          console.log(
            '🔍 Verification - process.env has:',
            // @ts-ignore - process.env exists in Node compat mode
            envKeys.map((k) => `${k}=${globalThis.process.env[k]?.substring(0, 10)}...`)
          );
        }
      } else {
        console.log('⚠️  No env vars provided in request');
      }
    } else {
      console.log('⚠️  No env object in request body');
    }

    // Execute the tool with AI SDK execution context
    // Some tools expect a second argument with { abortSignal, ... }
    const abortController = new AbortController();
    const executionContext = {
      abortSignal: abortController.signal,
      // Add other context properties that AI SDK tools might expect
      messages: [],
      toolCallId: `exec_${Date.now()}`,
    };

    console.log(`🚀 Executing ${cacheKey} with params:`, params);
    const result = await toolModule.execute(params || {}, executionContext);

    const executionTimeMs = Date.now() - startTime;
    console.log(`✅ Execution complete in ${executionTimeMs}ms`);

    // Report successful execution to health service (non-blocking)
    reportToolHealth(packageName, exportName, true).catch(() => {});

    return Response.json({
      success: true,
      output: result,
      executionTimeMs,
    });
  } catch (error) {
    const executionTimeMs = Date.now() - startTime;
    console.error('❌ Tool execution failed:', error);

    // Report failed execution to health service (non-blocking)
    reportToolHealth(packageName, exportName, false, error.message).catch(() => {});

    return Response.json(
      {
        success: false,
        error: error.message,
        executionTimeMs,
      },
      { status: 500 }
    );
  }
}

/**
 * Health check
 */
function health(): Response {
  return Response.json({
    status: 'ok',
    timestamp: new Date().toISOString(),
    cacheSize: moduleCache.size,
    denoVersion: Deno.version.deno,
    v8Version: Deno.version.v8,
    httpImports: true,
  });
}

/**
 * Cache stats
 */
function cacheStats(): Response {
  const entries = Array.from(moduleCache.keys());

  return Response.json({
    success: true,
    cacheSize: moduleCache.size,
    cachedTools: entries,
  });
}

/**
 * Clear cache
 */
function clearCache(): Response {
  const size = moduleCache.size;
  moduleCache.clear();
  console.log(`🗑️  Cleared cache (${size} entries)`);

  return Response.json({
    success: true,
    message: `Cleared ${size} cached modules`,
  });
}

/**
 * Main request handler
 */
async function handler(req: Request): Promise<Response> {
  const url = new URL(req.url);

  // Add CORS headers
  const headers = {
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
    'Access-Control-Allow-Headers': 'Content-Type',
  };

  if (req.method === 'OPTIONS') {
    return new Response(null, { headers });
  }

  try {
    let response: Response;

    if (url.pathname === '/health' && req.method === 'GET') {
      response = health();
    } else if (url.pathname === '/load-and-describe' && req.method === 'POST') {
      response = await loadAndDescribe(req);
    } else if (url.pathname === '/execute-tool' && req.method === 'POST') {
      response = await executeTool(req);
    } else if (url.pathname === '/cache/stats' && req.method === 'GET') {
      response = cacheStats();
    } else if (url.pathname === '/cache/clear' && req.method === 'POST') {
      response = clearCache();
    } else {
      response = Response.json({ error: 'Not found' }, { status: 404 });
    }

    // Add CORS headers to response
    Object.entries(headers).forEach(([key, value]) => {
      response.headers.set(key, value);
    });

    return response;
  } catch (error) {
    console.error('Request handler error:', error);
    return Response.json(
      {
        success: false,
        error: error.message,
      },
      { status: 500, headers }
    );
  }
}

// Start server
const port = Number.parseInt(Deno.env.get('PORT') || '3002');

console.log(`🚀 Railway Tool Executor (Deno) running on port ${port}`);
console.log('📦 HTTP imports: ENABLED');
console.log(`🔗 Health check: http://localhost:${port}/health`);
console.log('🛠️  Endpoints:');
console.log('   POST /load-and-describe - Load tool and get schema');
console.log('   POST /execute-tool - Execute a tool with params');
console.log('   POST /cache/clear - Clear module cache');
console.log('   GET /cache/stats - Get cache statistics');

Deno.serve({ port }, handler);
