import { OAuthCreateTokenHandler } from '@app/o-auth/flows/application/create-token/o-auth-create-token.handler';
import type {
  OAuthCreateTokenInput,
  OAuthCreateTokenResponse,
} from '@app/o-auth/flows/domain/flows.types';
import { oAuthParseBasicAuth } from '@app/o-auth/shared';
import {
  Body,
  Controller,
  Header,
  Headers,
  Post,
  Req,
  UnsupportedMediaTypeException,
  UseGuards,
} from '@nestjs/common';
import { Throttle, ThrottlerGuard } from '@nestjs/throttler';
import type { Request } from 'express';

const THROTTLE_LIMIT = Number(process.env.THROTTLE_OAUTH_TOKEN_LIMIT ?? 5);
const THROTTLE_TTL_MS = Number(process.env.THROTTLE_OAUTH_TOKEN_TTL_MS ?? 60_000);

/**
 * REST endpoint `POST /api/o-auth/token` per RFC 6749. Pure transport layer:
 *   - Enforces `application/x-www-form-urlencoded` (RFC 6749 §4.3 wire format).
 *   - Decodes HTTP Basic client credentials (RFC 6749 §2.3.1 transport encoding).
 *   - Normalizes the wire `grant_type` string to the canonical `OAuthCreateTokenInput.grantType`.
 *   - Extracts request metadata (ipAddress, userAgent) for audit logging.
 *
 * ALL business logic — client authentication, grant-field validation, dispatch —
 * lives in `OAuthCreateTokenHandler`. The controller passes empty strings for missing
 * fields and lets the handler raise the RFC 6749 §5.2 errors.
 *
 * See skill `catalyst-handler-composer` → "Composite handlers" section.
 */
@Controller('o-auth')
export class OAuthCreateTokenController {
  constructor(private readonly handler: OAuthCreateTokenHandler) {}

  @Post('token')
  @UseGuards(ThrottlerGuard)
  @Throttle({ default: { limit: THROTTLE_LIMIT, ttl: THROTTLE_TTL_MS } })
  // RFC 6749 §5.1 — token responses must be served with no-store/no-cache headers so that
  // intermediaries do not retain credentials.
  @Header('Cache-Control', 'no-store')
  @Header('Pragma', 'no-cache')
  async main(
    @Headers('authorization') authorization: string | undefined,
    @Headers('content-type') contentType: string | undefined,
    @Headers('user-agent') userAgent: string | undefined,
    @Req() req: Request,
    @Body() body: Record<string, unknown>,
  ): Promise<OAuthCreateTokenResponse> {
    if (!contentType || !contentType.toLowerCase().includes('application/x-www-form-urlencoded')) {
      // Pure HTTP concern: the wire format requirement. NOT an RFC 6749 §5.2 OAuth error.
      throw new UnsupportedMediaTypeException();
    }

    const { clientId, clientSecret } = oAuthParseBasicAuth(authorization);
    const input: OAuthCreateTokenInput = {
      grantType: normalizeGrantType(body.grant_type),
      rawGrantType: typeof body.grant_type === 'string' ? body.grant_type : undefined,
      clientId,
      clientSecret,
      username: typeof body.username === 'string' ? body.username : undefined,
      password: typeof body.password === 'string' ? body.password : undefined,
      refreshToken: typeof body.refresh_token === 'string' ? body.refresh_token : undefined,
      // Authorization Code grant (RFC 6749 §4.1.3 + RFC 7636 PKCE)
      code: typeof body.code === 'string' ? body.code : undefined,
      redirectUri: typeof body.redirect_uri === 'string' ? body.redirect_uri : undefined,
      codeVerifier: typeof body.code_verifier === 'string' ? body.code_verifier : undefined,
      ipAddress: req.ip ?? null,
      userAgent: userAgent ?? null,
    };

    return this.handler.main(input);
  }
}

/**
 * RFC 6749 wire-format `grant_type` strings → canonical `OAuthCreateTokenInput.grantType` enum.
 * This is pure transport mapping; the supported set is the handler's concern, expressed
 * by the `'UNSUPPORTED'` fallback that the handler rejects.
 */
function normalizeGrantType(raw: unknown): OAuthCreateTokenInput['grantType'] {
  if (raw === 'password') return 'PASSWORD';
  if (raw === 'refresh_token') return 'REFRESH_TOKEN';
  if (raw === 'authorization_code') return 'AUTHORIZATION_CODE';
  return 'UNSUPPORTED';
}
