/**
 * Token Controller for SentryAgent.ai AgentIdP.
 * HTTP handlers for POST /token, POST /token/introspect, POST /token/revoke.
 * Parses application/x-www-form-urlencoded bodies.
 * Returns OAuth2ErrorResponse for /token errors, ErrorResponse for introspect/revoke.
 */

import { Request, Response, NextFunction } from 'express';
import { OAuth2Service } from '../services/OAuth2Service.js';
import { tokenRequestSchema, introspectRequestSchema, revokeRequestSchema } from '../utils/validators.js';
import {
  AuthenticationError,
  AuthorizationError,
  FreeTierLimitError,
} from '../utils/errors.js';
import { ITokenRequest, IIntrospectRequest, IRevokeRequest, IOAuth2ErrorResponse } from '../types/index.js';

/**
 * Maps an error from the token issuance flow to an OAuth2ErrorResponse.
 *
 * @param err - The error to map.
 * @returns Object with error, error_description, and httpStatus.
 */
function mapToOAuth2Error(err: unknown): {
  body: IOAuth2ErrorResponse;
  httpStatus: number;
} {
  if (err instanceof FreeTierLimitError) {
    return {
      body: {
        error: 'unauthorized_client',
        error_description: err.message,
      },
      httpStatus: 403,
    };
  }
  if (err instanceof AuthorizationError) {
    return {
      body: {
        error: 'unauthorized_client',
        error_description: err.message,
      },
      httpStatus: 403,
    };
  }
  if (err instanceof AuthenticationError) {
    return {
      body: {
        error: 'invalid_client',
        error_description: 'Client authentication failed. Invalid client_id or client_secret.',
      },
      httpStatus: 401,
    };
  }
  // Default: internal server error
  return {
    body: {
      error: 'invalid_request',
      error_description: 'An unexpected error occurred.',
    },
    httpStatus: 500,
  };
}

/**
 * Controller for the OAuth 2.0 Token endpoints.
 */
export class TokenController {
  /**
   * @param oauth2Service - The OAuth2 token service.
   */
  constructor(private readonly oauth2Service: OAuth2Service) {}

  /**
   * Handles POST /token — issues an access token via Client Credentials grant.
   * Accepts application/x-www-form-urlencoded body.
   * Returns OAuth2ErrorResponse on failure.
   *
   * @param req - Express request with form-encoded body.
   * @param res - Express response.
   * @param next - Express next function.
   */
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  issueToken = async (req: Request, res: Response, _next: NextFunction): Promise<void> => {
    try {
      const body = req.body as ITokenRequest;

      // Validate grant_type first
      if (!body.grant_type) {
        res.status(400).json({
          error: 'invalid_request',
          error_description: "The 'grant_type' parameter is required.",
        } as IOAuth2ErrorResponse);
        return;
      }

      if (body.grant_type !== 'client_credentials') {
        res.status(400).json({
          error: 'unsupported_grant_type',
          error_description: "Only 'client_credentials' grant type is supported.",
        } as IOAuth2ErrorResponse);
        return;
      }

      // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
      const { error, value } = tokenRequestSchema.validate(body, { abortEarly: false });
      if (error) {
        res.status(400).json({
          error: 'invalid_request',
          error_description: error.details.map((d) => d.message).join('; '),
        } as IOAuth2ErrorResponse);
        return;
      }

      const tokenBody = value as ITokenRequest;

      // Support HTTP Basic auth fallback
      let clientId = tokenBody.client_id;
      let clientSecret = tokenBody.client_secret;

      const authHeader = req.headers['authorization'];
      if (authHeader?.startsWith('Basic ')) {
        const base64 = authHeader.slice(6);
        const decoded = Buffer.from(base64, 'base64').toString('utf-8');
        const colonIndex = decoded.indexOf(':');
        if (colonIndex !== -1) {
          clientId = decoded.slice(0, colonIndex);
          clientSecret = decoded.slice(colonIndex + 1);
        }
      }

      if (!clientId || !clientSecret) {
        res.status(400).json({
          error: 'invalid_request',
          error_description: "The 'client_id' and 'client_secret' parameters are required.",
        } as IOAuth2ErrorResponse);
        return;
      }

      // Validate requested scope
      const requestedScope = tokenBody.scope ?? 'agents:read';
      const validScopes = ['agents:read', 'agents:write', 'tokens:read', 'audit:read'];
      const scopeList = requestedScope.split(' ');
      const invalidScope = scopeList.find((s) => !validScopes.includes(s));
      if (invalidScope) {
        res.status(400).json({
          error: 'invalid_scope',
          error_description: `Requested scope '${invalidScope}' is not available.`,
        } as IOAuth2ErrorResponse);
        return;
      }

      const ipAddress = req.ip ?? '0.0.0.0';
      const userAgent = req.headers['user-agent'] ?? 'unknown';

      const tokenResponse = await this.oauth2Service.issueToken(
        clientId,
        clientSecret,
        requestedScope,
        ipAddress,
        userAgent,
      );

      res.setHeader('Cache-Control', 'no-store');
      res.setHeader('Pragma', 'no-cache');
      res.status(200).json(tokenResponse);
    } catch (err) {
      // Token endpoint uses OAuth2ErrorResponse format
      const mapped = mapToOAuth2Error(err);
      res.status(mapped.httpStatus).json(mapped.body);
    }
  };

  /**
   * Handles POST /token/introspect — introspects a token per RFC 7662.
   * Requires Bearer auth with tokens:read scope.
   *
   * @param req - Express request.
   * @param res - Express response.
   * @param next - Express next function.
   */
  introspectToken = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
    try {
      if (!req.user) {
        throw new AuthenticationError();
      }

      // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
      const { error, value } = introspectRequestSchema.validate(req.body, { abortEarly: false });
      if (error) {
        const messages = error.details.map((d) => d.message).join('; ');
        throw new Error(messages);
      }

      const body = value as IIntrospectRequest;
      const ipAddress = req.ip ?? '0.0.0.0';
      const userAgent = req.headers['user-agent'] ?? 'unknown';

      const result = await this.oauth2Service.introspectToken(
        body.token,
        req.user,
        ipAddress,
        userAgent,
      );

      res.status(200).json(result);
    } catch (err) {
      next(err);
    }
  };

  /**
   * Handles POST /token/revoke — revokes a token per RFC 7009.
   * Requires Bearer auth.
   *
   * @param req - Express request.
   * @param res - Express response.
   * @param next - Express next function.
   */
  revokeToken = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
    try {
      if (!req.user) {
        throw new AuthenticationError();
      }

      // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
      const { error, value } = revokeRequestSchema.validate(req.body, { abortEarly: false });
      if (error) {
        const messages = error.details.map((d) => d.message).join('; ');
        throw new Error(messages);
      }

      const body = value as IRevokeRequest;
      const ipAddress = req.ip ?? '0.0.0.0';
      const userAgent = req.headers['user-agent'] ?? 'unknown';

      await this.oauth2Service.revokeToken(body.token, req.user, ipAddress, userAgent);
      res.status(200).json({});
    } catch (err) {
      next(err);
    }
  };
}
