import { Body, Controller, Get, HttpCode, Post, Query, Req, Res, Version, VERSION_NEUTRAL } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Request, Response } from 'express';
import * as crypto from 'crypto';

type AuthorizationCodeRecord = {
  code: string;
  clientId: string;
  redirectUri: string;
  codeChallenge?: string;
  codeChallengeMethod?: string;
  state?: string;
  userId: string;
  expiresAt: number;
};

@Controller()
export class McpOauthController {
  private readonly authorizationCodes = new Map<string, AuthorizationCodeRecord>();

  constructor(private readonly jwtService: JwtService) { }

  private getBaseUrl(req: Request): string {
    const configuredBaseUrl = String(process.env.MCP_PUBLIC_BASE_URL || '').trim();
    if (configuredBaseUrl) {
      return configuredBaseUrl.replace(/\/+$/, '');
    }

    const forwardedProto = req.headers['x-forwarded-proto'];
    const forwardedHost = req.headers['x-forwarded-host'];

    const rawProto = Array.isArray(forwardedProto) ? forwardedProto[0] : forwardedProto;
    const rawHost = Array.isArray(forwardedHost) ? forwardedHost[0] : forwardedHost;

    const proto = String(rawProto || req.protocol || 'https')
      .split(',')[0]
      .trim()
      .toLowerCase();
    const host = String(rawHost || req.get('host') || '')
      .split(',')[0]
      .trim();

    return `${proto}://${host}`;
  }

  private protectedResourceMetadata(req: Request) {
    const baseUrl = this.getBaseUrl(req);
    return {
      resource: `${baseUrl}/v1/mcp`,
      authorization_servers: [baseUrl],
      bearer_methods_supported: ['header'],
      scopes_supported: ['mcp.read'],
    };
  }

  private authorizationServerMetadata(req: Request) {
    const baseUrl = this.getBaseUrl(req);
    return {
      issuer: baseUrl,
      authorization_endpoint: `${baseUrl}/v1/oauth/authorize`,
      token_endpoint: `${baseUrl}/v1/oauth/token`,
      registration_endpoint: `${baseUrl}/v1/oauth/register`,
      revocation_endpoint: `${baseUrl}/v1/oauth/revoke`,
      response_types_supported: ['code'],
      grant_types_supported: ['authorization_code', 'client_credentials'],
      token_endpoint_auth_methods_supported: ['none', 'client_secret_post'],
      code_challenge_methods_supported: ['S256'],
      scopes_supported: ['mcp.read'],
    };
  }

  @Version(VERSION_NEUTRAL)
  @Get('.well-known/oauth-protected-resource')
  getProtectedResourceMetadataRoot(@Req() req: Request, @Res() res: Response) {
    return res.status(200).json(this.protectedResourceMetadata(req));
  }

  @Version(VERSION_NEUTRAL)
  @Get('.well-known/openai-apps-challenge')
  getOpenAiAppsChallengeRoot(@Res() res: Response) {
    const token = String(process.env.OPENAI_APPS_CHALLENGE_TOKEN || '').trim();
    if (!token) {
      return res.status(404).json({
        error: 'not_configured',
        error_description: 'OPENAI_APPS_CHALLENGE_TOKEN is not configured.',
      });
    }
    return res.status(200).type('text/plain').send(token);
  }

  @Get('.well-known/oauth-protected-resource')
  getProtectedResourceMetadataV1(@Req() req: Request, @Res() res: Response) {
    return res.status(200).json(this.protectedResourceMetadata(req));
  }

  @Get('.well-known/openai-apps-challenge')
  getOpenAiAppsChallengeV1(@Res() res: Response) {
    const token = String(process.env.OPENAI_APPS_CHALLENGE_TOKEN || '').trim();
    if (!token) {
      return res.status(404).json({
        error: 'not_configured',
        error_description: 'OPENAI_APPS_CHALLENGE_TOKEN is not configured.',
      });
    }
    return res.status(200).type('text/plain').send(token);
  }

  @Version(VERSION_NEUTRAL)
  @Get('.well-known/oauth-authorization-server')
  getAuthorizationServerMetadataRoot(@Req() req: Request, @Res() res: Response) {
    return res.status(200).json(this.authorizationServerMetadata(req));
  }

  @Get('.well-known/oauth-authorization-server')
  getAuthorizationServerMetadataV1(@Req() req: Request, @Res() res: Response) {
    return res.status(200).json(this.authorizationServerMetadata(req));
  }

  @Version(VERSION_NEUTRAL)
  @Get('oauth/authorize')
  authorizeRoot(@Req() req: Request, @Res() res: Response) {
    return this.handleAuthorize(req, res);
  }

  @Get('oauth/authorize')
  authorizeV1(@Req() req: Request, @Res() res: Response) {
    return this.handleAuthorize(req, res);
  }

  @Version(VERSION_NEUTRAL)
  @Post('oauth/register')
  registerClientRoot(@Res() res: Response) {
    return res.status(200).json({
      client_id: process.env.MCP_OAUTH_CLIENT_ID || 'connectorclub-mcp-client',
      token_endpoint_auth_method: 'none',
      grant_types: ['authorization_code', 'client_credentials'],
      response_types: ['code'],
    });
  }

  @Post('oauth/register')
  registerClientV1(@Res() res: Response) {
    return res.status(200).json({
      client_id: process.env.MCP_OAUTH_CLIENT_ID || 'connectorclub-mcp-client',
      token_endpoint_auth_method: 'none',
      grant_types: ['authorization_code', 'client_credentials'],
      response_types: ['code'],
    });
  }

  @Version(VERSION_NEUTRAL)
  @Post('oauth/token')
  @HttpCode(200)
  tokenRoot(@Body() body: any, @Res() res: Response) {
    return this.issueToken(body, res);
  }

  @Post('oauth/token')
  @HttpCode(200)
  tokenV1(@Body() body: any, @Res() res: Response) {
    return this.issueToken(body, res);
  }

  private issueToken(body: any, res: Response) {
    if (body?.grant_type === 'authorization_code') {
      const code = String(body?.code || '');
      const redirectUri = String(body?.redirect_uri || '');
      const clientId = String(body?.client_id || process.env.MCP_OAUTH_CLIENT_ID || 'connectorclub-mcp-client');
      const codeVerifier = String(body?.code_verifier || '');

      const codeRecord = this.authorizationCodes.get(code);
      if (!codeRecord) {
        return res.status(400).json({
          error: 'invalid_grant',
          error_description: 'Authorization code is invalid or expired.',
        });
      }

      if (Date.now() > codeRecord.expiresAt) {
        this.authorizationCodes.delete(code);
        return res.status(400).json({
          error: 'invalid_grant',
          error_description: 'Authorization code has expired.',
        });
      }

      if (codeRecord.clientId !== clientId || codeRecord.redirectUri !== redirectUri) {
        return res.status(400).json({
          error: 'invalid_grant',
          error_description: 'Authorization code does not match client or redirect URI.',
        });
      }

      if (codeRecord.codeChallenge) {
        if (!codeVerifier) {
          return res.status(400).json({
            error: 'invalid_request',
            error_description: 'Missing code_verifier.',
          });
        }
        const verifierDigest = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
        if (verifierDigest !== codeRecord.codeChallenge) {
          return res.status(400).json({
            error: 'invalid_grant',
            error_description: 'Invalid code_verifier.',
          });
        }
      }

      this.authorizationCodes.delete(code);
      const accessToken = this.jwtService.sign(
        {
          sub: codeRecord.userId,
          mcp_member: true,
          scope: 'mcp.read',
        },
        { expiresIn: '1h' },
      );

      return res.json({
        token_type: 'Bearer',
        access_token: accessToken,
        expires_in: 3600,
        scope: 'mcp.read',
      });
    }

    if (body?.grant_type !== 'client_credentials') {
      return res.status(400).json({
        error: 'unsupported_grant_type',
        error_description: 'Supported grants: authorization_code, client_credentials.',
      });
    }

    const accessToken = this.jwtService.sign(
      {
        sub: 'mcp-service-user',
        mcp_member: true,
        scope: 'mcp.read',
      },
      { expiresIn: '1h' },
    );

    return res.json({
      token_type: 'Bearer',
      access_token: accessToken,
      expires_in: 3600,
      scope: 'mcp.read',
    });
  }

  @Version(VERSION_NEUTRAL)
  @Get('oauth/callback')
  callbackRoot(@Req() req: Request, @Res() res: Response, @Query() query: any) {
    return this.handleCallback(req, res, query);
  }

  @Get('oauth/callback')
  callbackV1(@Req() req: Request, @Res() res: Response, @Query() query: any) {
    return this.handleCallback(req, res, query);
  }

  private handleAuthorize(req: Request, res: Response) {
    const baseUrl = this.getBaseUrl(req);
    const frontendAuthorizeUrl =
      process.env.MCP_FRONTEND_OAUTH_AUTHORIZE_URL || `${baseUrl}/login`;

    const responseType = String(req.query?.response_type || '');
    const clientId = String(req.query?.client_id || '');
    const redirectUri = String(req.query?.redirect_uri || '');
    const state = req.query?.state ? String(req.query.state) : '';
    const codeChallenge = req.query?.code_challenge
      ? String(req.query.code_challenge)
      : '';
    const codeChallengeMethod = req.query?.code_challenge_method
      ? String(req.query.code_challenge_method)
      : '';
    const resource = req.query?.resource ? String(req.query.resource) : '';

    if (responseType !== 'code' || !clientId || !redirectUri) {
      return res.status(400).json({
        error: 'invalid_request',
        error_description: 'Missing required query params: response_type=code, client_id, redirect_uri.',
      });
    }

    const callbackUrl = new URL(`${baseUrl}/v1/oauth/callback`);
    callbackUrl.searchParams.set('client_id', clientId);
    callbackUrl.searchParams.set('redirect_uri', redirectUri);
    if (state) callbackUrl.searchParams.set('state', state);
    if (codeChallenge) callbackUrl.searchParams.set('code_challenge', codeChallenge);
    if (codeChallengeMethod) callbackUrl.searchParams.set('code_challenge_method', codeChallengeMethod);
    if (resource) callbackUrl.searchParams.set('resource', resource);

    const targetUrl = new URL(frontendAuthorizeUrl);
    targetUrl.searchParams.set('next', callbackUrl.toString());
    targetUrl.searchParams.set('client_id', clientId);
    targetUrl.searchParams.set('redirect_uri', redirectUri);
    if (state) targetUrl.searchParams.set('state', state);
    if (resource) targetUrl.searchParams.set('resource', resource);

    return res.redirect(targetUrl.toString());
  }

  private handleCallback(req: Request, res: Response, query: any) {
    const idToken = query?.id_token ? String(query.id_token) : '';
    const accessToken = query?.access_token ? String(query.access_token) : '';
    const userToken = accessToken || idToken;

    const clientId = String(query?.client_id || '');
    const redirectUri = String(query?.redirect_uri || '');
    const state = query?.state ? String(query.state) : '';
    const codeChallenge = query?.code_challenge ? String(query.code_challenge) : '';
    const codeChallengeMethod = query?.code_challenge_method
      ? String(query.code_challenge_method)
      : '';

    if (!userToken || !clientId || !redirectUri) {
      return res.status(400).json({
        error: 'invalid_request',
        error_description:
          'Missing required callback params. Expected access_token or id_token with client_id and redirect_uri.',
      });
    }

    try {
      const decoded = this.jwtService.verify<any>(userToken);
      const userId = String(decoded?.sub || '');
      if (!userId) {
        return res.status(401).json({
          error: 'invalid_token',
          error_description: 'User token missing subject.',
        });
      }

      const code = crypto.randomBytes(24).toString('base64url');
      this.authorizationCodes.set(code, {
        code,
        clientId,
        redirectUri,
        codeChallenge: codeChallenge || undefined,
        codeChallengeMethod: codeChallengeMethod || undefined,
        state: state || undefined,
        userId,
        expiresAt: Date.now() + 5 * 60 * 1000,
      });

      const redirectTarget = new URL(redirectUri);
      redirectTarget.searchParams.set('code', code);
      if (state) redirectTarget.searchParams.set('state', state);
      return res.redirect(redirectTarget.toString());
    } catch (error) {
      return res.status(401).json({
        error: 'invalid_token',
        error_description: 'Provided user token is invalid or expired.',
      });
    }
  }
}
