/* eslint-disable */
// WARNING: This file was auto-generated by tsoa-mcp. Do not modify it manually.
// Re-run `tsoa-mcp spec-and-routes` to regenerate.

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import type { Request, Response, Router, RequestHandler } from 'express';
import { z } from 'zod';
import { tsoaRouteRegistry } from '{{{routesImportPath}}}';
import { resolveTools, mountMcpOAuth, type McpToolEntry, type McpOAuthConfig } from 'tsoa-mcp';

// Zod schemas generated by Orval from swagger.json
import * as schemas from '{{{schemasImportPath}}}';

// Map operationId -> Zod input schema (merged from query + path + body params)
const toolInputSchemas: Record<string, Record<string, z.ZodType>> = {
{{#each operations}}
  '{{operationId}}': {
    {{#if queryParamsSchema}}
    ...('{{queryParamsSchema}}' in schemas ? (schemas as any)['{{queryParamsSchema}}'].shape : {}),
    {{/if}}
    {{#if pathParamsSchema}}
    ...('{{pathParamsSchema}}' in schemas ? (schemas as any)['{{pathParamsSchema}}'].shape : {}),
    {{/if}}
    {{#if bodySchema}}
    ...('{{bodySchema}}' in schemas ? (schemas as any)['{{bodySchema}}'].shape : {}),
    {{/if}}
  },
{{/each}}
};

// Map operationId -> Zod header schema (for non-OAuth mode where headers become tool params)
const toolHeaderSchemas: Record<string, Record<string, z.ZodType>> = {
{{#each operations}}
  {{#if headerSchema}}
  '{{operationId}}': {
    ...('{{headerSchema}}' in schemas ? (schemas as any)['{{headerSchema}}'].shape : {}),
  },
  {{/if}}
{{/each}}
};

export interface McpErrorContext {
  oauthActive: boolean;
  toolOAuthMode: import('tsoa-mcp').MCPOAuthMode;
}
export type McpErrorMapperFn = (err: any, context: McpErrorContext) => { status: number; message: string } | null;

interface RegisterMCPRoutesOptions {
  name: string;
  version: string;
  path?: string;
  /** Instructions sent to the MCP client describing what this server does */
  instructions?: string;
  /** Express middlewares to run before the MCP handler (e.g. auth, error mapping) */
  middlewares?: RequestHandler[];
  /** Map caught errors to custom status/message. Return null to use default handling. */
  errorMapper?: McpErrorMapperFn;
  /** OAuth configuration. When enabled, mounts OAuth endpoints and requires Bearer token on POST /mcp. */
  oauth?: McpOAuthConfig & { enabled: boolean };
}

/**
 * Register MCP tool routes on an Express app.
 * Only @Tool-decorated controller methods are exposed.
 * Runs in stateless mode — safe for distributed deployments.
 *
 * Uses invokeDirect to call controller methods directly — no fake res needed.
 */
export function RegisterMCPRoutes(app: Router, opts: RegisterMCPRoutesOptions): void {
  const tools = resolveTools(tsoaRouteRegistry);
  const mcpPath = opts.path ?? '/mcp';

  console.log(`[tsoa-mcp] Mounting stateless MCP server "${opts.name}" at ${mcpPath}`);
  console.log(`[tsoa-mcp] ${tools.length} @Tool-decorated routes registered`);
  for (const tool of tools) {
    const schema = findSchemaForTool(tool);
    console.log(`[tsoa-mcp]   - ${tool.name} (${tool.method.toUpperCase()} ${tool.path}) schema=${schema ? Object.keys(schema).join(', ') : '(none)'}`);
  }

  // Mount OAuth endpoints (authorize, token, register, well-known) before the MCP handler
  if (opts.oauth?.enabled) {
    mountMcpOAuth(app, mcpPath, opts.oauth);
  }

  // Build middleware chain for POST /mcp
  const mcpMiddlewares: RequestHandler[] = [...(opts.middlewares ?? [])];

  // POST /mcp — stateless: each request gets a fresh server + transport
  app.post(mcpPath, ...mcpMiddlewares, async (req: Request, res: Response) => {
    const oauthActive = req.query.oauth === 'true';

    // If oauth=true, require Bearer token
    if (oauthActive && opts.oauth?.enabled) {
      const hasBearer = req.headers['authorization']?.startsWith('Bearer ');
      const hasOAuthToken = !!req.headers['x-oauth-token'];
      if (!hasBearer && !hasOAuthToken) {
        const resourceUrl = `${req.get('x-forwarded-proto') || req.protocol}://${req.get('host')}/.well-known/oauth-protected-resource`;
        res.status(401)
          .set('WWW-Authenticate', `Bearer resource_metadata="${resourceUrl}"`)
          .json({ error: 'unauthorized', error_description: 'OAuth authentication required' });
        return;
      }

      // Validate the token if a validator is configured
      if (opts.oauth.tokenValidator) {
        const token = hasBearer
          ? (req.headers['authorization'] as string).slice(7)
          : req.headers['x-oauth-token'] as string;
        const isValid = await opts.oauth.tokenValidator(token);
        if (!isValid) {
          const resourceUrl = `${req.get('x-forwarded-proto') || req.protocol}://${req.get('host')}/.well-known/oauth-protected-resource`;
          res.status(401)
            .set('WWW-Authenticate', `Bearer resource_metadata="${resourceUrl}"`)
            .json({ error: 'unauthorized', error_description: 'OAuth token is expired or invalid' });
          return;
        }
      }
    }

    try {
      const transport = new StreamableHTTPServerTransport({
        sessionIdGenerator: undefined,
      });

      const server = new McpServer({
        name: opts.name,
        version: opts.version,
        ...(opts.instructions ? { instructions: opts.instructions } : {}),
      });

      for (const tool of tools) {
        // Hide @MCPOAuth(true) tools when oauth is not active
        if (tool.oauth === 'required' && !oauthActive) continue;

        // When oauth is not active and tool supports OAuth, include header params as tool inputs
        const includeHeaders = tool.oauth !== false && !oauthActive;
        const schema = findSchemaForTool(tool, includeHeaders);
        const requiredScopes = extractSecurityScopes(tool.security);

        server.registerTool(
          tool.name,
          {
            description: tool.description,
            ...(schema && Object.keys(schema).length > 0 ? { inputSchema: schema } : {}),
            ...(requiredScopes.length > 0 ? { _meta: { requiredScopes } } : {}),
          },
          async (params: Record<string, unknown>) => {
            return await invokeTool(tool, params, req, oauthActive, opts.errorMapper);
          },
        );
      }

      await server.connect(transport);
      await transport.handleRequest(req, res, req.body);

      res.on('finish', () => {
        transport.close();
        server.close();
      });
    } catch (err) {
      console.error('[tsoa-mcp] Error handling POST:', err);
      if (!res.headersSent) {
        res.status(500).json({
          jsonrpc: '2.0',
          error: { code: -32603, message: 'Internal server error' },
          id: null,
        });
      }
    }
  });

  app.get(mcpPath, (_req: Request, res: Response) => {
    res.status(405).json({
      jsonrpc: '2.0',
      error: { code: -32000, message: 'Method not allowed in stateless mode' },
      id: null,
    });
  });

  app.delete(mcpPath, (_req: Request, res: Response) => {
    res.status(405).json({
      jsonrpc: '2.0',
      error: { code: -32000, message: 'Method not allowed in stateless mode' },
      id: null,
    });
  });
}

/** Extract unique scope strings from TSOA security definitions (e.g. ["PRO", "WEBCAST_PREMIUM"]). */
function extractSecurityScopes(security: any[]): string[] {
  const scopes = new Set<string>();
  for (const req of security) {
    for (const schemes of Object.values(req)) {
      if (Array.isArray(schemes)) {
        for (const s of schemes) if (typeof s === 'string') scopes.add(s);
      }
    }
  }
  return [...scopes];
}

function findSchemaForTool(tool: McpToolEntry, includeHeaders: boolean = false): Record<string, z.ZodType> | undefined {
  // TSOA operationId is the method name PascalCased (e.g. kickRoomUser -> KickRoomUser)
  // Orval schema names follow the operationId from swagger.json
  const pascalMethod = tool.methodName.charAt(0).toUpperCase() + tool.methodName.slice(1);
  const candidates = [
    pascalMethod,                                     // KickRoomUser (operationId = PascalCased method name)
    tool.controllerName,                              // RetrieveRoomId (when class name matches operationId)
    tool.methodName,                                  // retrieveRoomId
    `${tool.controllerName}_${tool.methodName}`,      // RetrieveRoomId_retrieveRoomId
  ];

  let baseSchema: Record<string, z.ZodType> | undefined;
  let headerSchema: Record<string, z.ZodType> | undefined;

  for (const candidate of candidates) {
    if (!baseSchema && candidate in toolInputSchemas) {
      const schema = toolInputSchemas[candidate];
      if (Object.keys(schema).length > 0) baseSchema = schema;
    }
    if (includeHeaders && !headerSchema && candidate in toolHeaderSchemas) {
      headerSchema = toolHeaderSchemas[candidate];
    }
  }

  if (!baseSchema && !headerSchema) return undefined;
  if (!includeHeaders || !headerSchema) return baseSchema;
  return { ...(baseSchema ?? {}), ...headerSchema };
}

/**
 * Invoke a tool by calling the controller method directly via invokeDirect.
 *
 * 1. Mutate the real req to carry the tool's params
 * 2. Run the tool's auth middleware chain against the real req
 * 3. Call invokeDirect — validates args via TSOA, calls the controller, returns the result
 * 4. Run the resolver to transform the output
 */
async function invokeTool(
  tool: McpToolEntry,
  params: Record<string, unknown>,
  originalReq: Request,
  oauthActive: boolean,
  errorMapper?: McpErrorMapperFn,
): Promise<{ content: Array<{ type: 'text'; text: string }>; isError?: boolean }> {
  const reqData = buildRequestData(tool, params);

  // Save originals
  const saved = {
    query: originalReq.query,
    params: originalReq.params,
    body: originalReq.body,
    path: originalReq.path,
    method: originalReq.method,
    url: originalReq.url,
  };

  // Force-set properties (Express 5 has getter-only query)
  function forceSet(obj: any, key: string, value: any) {
    Object.defineProperty(obj, key, { value, writable: true, configurable: true, enumerable: true });
  }

  forceSet(originalReq, 'query', reqData.query);
  forceSet(originalReq, 'params', reqData.params);
  forceSet(originalReq, 'body', reqData.body);
  forceSet(originalReq, 'path', tool.path);
  forceSet(originalReq, 'method', tool.method.toUpperCase());
  forceSet(originalReq, 'url', tool.path);

  // MCP OAuth: map Authorization Bearer token to x-oauth-token for controllers
  const savedHeaders: Record<string, string | string[] | undefined> = {};
  savedHeaders['x-oauth-token'] = originalReq.headers['x-oauth-token'];
  const authHeader = originalReq.headers['authorization'];
  if (!originalReq.headers['x-oauth-token'] && authHeader && typeof authHeader === 'string' && authHeader.startsWith('Bearer ')) {
    originalReq.headers['x-oauth-token'] = authHeader.slice(7);
  }

  // Apply header params from tool input (e.g. cookieHeader when OAuth is not active)
  for (const [headerName, headerValue] of Object.entries(reqData.headers)) {
    if (!(headerName in savedHeaders)) {
      savedHeaders[headerName] = originalReq.headers[headerName];
    }
    originalReq.headers[headerName] = headerValue;
  }

  function restore() {
    // Restore all modified headers
    for (const [headerName, original] of Object.entries(savedHeaders)) {
      if (original === undefined) {
        delete originalReq.headers[headerName];
      } else {
        originalReq.headers[headerName] = original;
      }
    }
    forceSet(originalReq, 'query', saved.query);
    forceSet(originalReq, 'params', saved.params);
    forceSet(originalReq, 'body', saved.body);
    forceSet(originalReq, 'path', saved.path);
    forceSet(originalReq, 'method', saved.method);
    forceSet(originalReq, 'url', saved.url);
  }

  // Override req.res with a stub so auth middleware doesn't write to the real MCP transport response.
  // The stub must track status(), json(), send(), and end() so we can detect when middleware
  // rejects the request (e.g. auth failure, rate limiting) by writing an error response.
  const savedRes = (originalReq as any).res;
  let resStubBody: any = undefined;
  const resStub: any = new Proxy({ writableEnded: false, headersSent: false, statusCode: 200 }, {
    get: (target, prop) => {
      if (prop in target) return (target as any)[prop];
      if (prop === 'status') return (code: number) => { (target as any).statusCode = code; return resStub; };
      if (prop === 'json' || prop === 'send') return (body: any) => { resStubBody = body; (target as any).headersSent = true; (target as any).writableEnded = true; return resStub; };
      if (prop === 'end') return (chunk?: any) => { if (chunk) resStubBody = chunk; (target as any).headersSent = true; (target as any).writableEnded = true; return resStub; };
      if (typeof prop === 'string') return (..._args: any[]) => resStub;
      return undefined;
    },
    set: (target, prop, value) => {
      (target as any)[prop] = value;
      return true;
    },
  });
  (originalReq as any).res = resStub;

  try {
    // Run auth middleware chain against the real request
    const middlewares = tool.getMiddlewares();
    console.log(`[tsoa-mcp] Invoking "${tool.name}" — ${middlewares.length} middleware(s) to run`);
    await runMiddlewareChain(originalReq, middlewares);
    console.log(`[tsoa-mcp] Auth passed for "${tool.name}" — account=${(originalReq as any).accountData?.id ?? 'N/A'}, key=${(originalReq as any).apiKeyData?.id ?? 'N/A'}`);

    // Check if middleware wrote an error response to req.res (e.g. sendUnauthorizedResponse)
    if (resStub.statusCode >= 400) {
      const errorText = resStubBody
        ? (typeof resStubBody === 'object' ? JSON.stringify(resStubBody) : String(resStubBody))
        : `Error ${resStub.statusCode}`;
      return {
        content: [{ type: 'text', text: `Error ${resStub.statusCode}: ${errorText}` }],
        isError: true,
      };
    }

    // Call the controller method directly — no fake res needed
    const directResult = await tool.invokeDirect(originalReq as any);
    const statusCode = directResult?.statusCode ?? 200;
    const body = directResult?.body;

    if (statusCode >= 400) {
      const errorText = typeof body === 'object'
        ? JSON.stringify(body)
        : String(body ?? `Error ${statusCode}`);
      return {
        content: [{ type: 'text', text: `Error ${statusCode}: ${errorText}` }],
        isError: true,
      };
    }

    // Run the resolver to transform the output
    const resolved = tool.resolver.resolve(body);
    const text = typeof resolved === 'object'
      ? JSON.stringify(resolved)
      : String(resolved ?? '');

    return { content: [{ type: 'text', text }] };
  } catch (err: any) {
    console.error(`[tsoa-mcp] Tool "${tool.name}" threw:`, err.name ?? 'Error', err.message ?? err, err.stack ? `\n${err.stack}` : '');

    // Allow custom error mapping (e.g. convert 422 missing_auth to 401)
    if (errorMapper) {
      const mapped = errorMapper(err, { oauthActive, toolOAuthMode: tool.oauth });
      if (mapped) {
        return {
          content: [{ type: 'text', text: `Error ${mapped.status}: ${mapped.message}` }],
          isError: true,
        };
      }
    }

    const status = err.status || err.statusCode || 500;
    let message = err.message || String(err);

    // TSOA ValidateError includes field-level details
    if (err.name === 'ValidateError' && err.fields) {
      const fieldErrors = Object.entries(err.fields)
        .map(([field, detail]: [string, any]) => `${field}: ${detail.message}`)
        .join(', ');
      message = `Validation failed: ${fieldErrors}`;
    }

    return {
      content: [{ type: 'text', text: `Error ${status}: ${message}` }],
      isError: true,
    };
  } finally {
    (originalReq as any).res = savedRes;
    restore();
  }
}

/**
 * Run an array of Express middlewares sequentially (promise-based).
 * Rejects if any middleware calls next(err) or throws.
 */
function runMiddlewareChain(req: any, middlewares: any[]): Promise<void> {
  return new Promise((resolve, reject) => {
    let i = 0;

    // Proxy stub for res — middlewares may call setHeader, status, json, etc.
    // Tracks status(), json(), send(), end() so we can detect when a middleware
    // sends a response directly (e.g. rate limiter 429) without calling next().
    const resStub: any = new Proxy({ writableEnded: false, headersSent: false, statusCode: 200 }, {
      get: (target, prop) => {
        if (prop in target) return (target as any)[prop];
        if (prop === 'status') return (code: number) => { (target as any).statusCode = code; return resStub; };
        if (prop === 'json' || prop === 'send') return (body: any) => { (target as any)._body = body; (target as any).headersSent = true; (target as any).writableEnded = true; return resStub; };
        if (prop === 'end') return (chunk?: any) => { if (chunk) (target as any)._body = chunk; (target as any).headersSent = true; (target as any).writableEnded = true; return resStub; };
        if (typeof prop === 'string') return (..._args: any[]) => resStub;
        return undefined;
      },
      set: (target, prop, value) => {
        (target as any)[prop] = value;
        return true;
      },
    });

    function next(err?: any) {
      if (err) {
        const prevName = i > 0 ? (middlewares[i - 1].name || `middleware[${i - 1}]`) : 'unknown';
        console.log(`[tsoa-mcp] Middleware[${i - 1}] "${prevName}" rejected:`, err.message ?? err);
        return reject(err);
      }
      if (i >= middlewares.length) return resolve();
      const mw = middlewares[i++];
      const mwName = mw.name || `middleware[${i - 1}]`;
      console.log(`[tsoa-mcp] Running ${mwName} (${i}/${middlewares.length})`);
      try {
        const result = mw(req, resStub, next);
        // Handle async middlewares
        if (result && typeof result.catch === 'function') {
          result.catch(reject);
        }
      } catch (e) {
        reject(e);
      }
    }

    next();
  });
}

function buildRequestData(tool: McpToolEntry, params: Record<string, unknown>): {
  params: Record<string, any>; query: Record<string, any>; body: any; headers: Record<string, string>;
} {
  const result = { params: {} as any, query: {} as any, body: {} as any, headers: {} as Record<string, string> };
  for (const [key, schema] of Object.entries(tool.args)) {
    if (schema.in === 'request') continue;
    const value = params[key];
    if (value === undefined) continue;
    switch (schema.in) {
      case 'path': result.params[key] = value; break;
      case 'query': case 'queries': result.query[key] = value; break;
      case 'body': result.body = value; break;
      case 'body-prop': result.body[key] = value; break;
      case 'formData': result.body[key] = value; break;
      case 'header': {
        // Map param name to header name (schema.name contains the actual header name like "x-cookie-header")
        const headerName = (schema as any).name || key;
        result.headers[headerName.toLowerCase()] = String(value);
        break;
      }
    }
  }
  return result;
}
