import { OAuthAuthorizeHandler } from '@app/o-auth/flows/application/authorize/o-auth-authorize.handler';
import { OAuthHubSessionService } from '@app/o-auth/flows/application/session/o-auth-hub-session.service';
import type {
  OAuthAuthorizeCodeResult,
  OAuthAuthorizeRedirectResult,
} from '@app/o-auth/flows/domain/flows.types';
import { Controller, Get, Query, Req, Res, UseGuards } from '@nestjs/common';
import { Throttle, ThrottlerGuard } from '@nestjs/throttler';
import type { Request, Response } from 'express';

const THROTTLE_LIMIT = Number(process.env.THROTTLE_OAUTH_AUTHORIZE_LIMIT ?? 20);
const THROTTLE_TTL_MS = Number(process.env.THROTTLE_OAUTH_AUTHORIZE_TTL_MS ?? 60_000);

/**
 * REST endpoint `GET /api/o-auth/authorize` per RFC 6749 §4.1.1 + RFC 7636 PKCE.
 *
 * Pure transport layer:
 *   - Reads RFC query parameters (client_id, redirect_uri, response_type, state, scope,
 *     code_challenge, code_challenge_method).
 *   - Reads the hub_session cookie (httpOnly, RS256 JWT) and extracts accountId. If absent
 *     or invalid, passes accountId=null to the handler (handler decides redirect to sign-in).
 *   - Delegates ALL business logic to OAuthAuthorizeHandler (validation, client resolution,
 *     PKCE validation, code minting).
 *   - Responds with 302 to the redirect URL returned by the handler.
 *
 * This endpoint is PUBLIC (no @Auth decorator) — authentication state is carried by the
 * hub_session cookie, not a Bearer token.
 *
 * Cookie parsing: the hub_session value is extracted from the Cookie header manually
 * (no cookie-parser middleware required — avoids a middleware-wide dependency for one cookie).
 */
@Controller('o-auth')
export class OAuthAuthorizeController {
  constructor(
    private readonly handler: OAuthAuthorizeHandler,
    private readonly sessionService: OAuthHubSessionService,
  ) {}

  @Get('authorize')
  @UseGuards(ThrottlerGuard)
  @Throttle({ default: { limit: THROTTLE_LIMIT, ttl: THROTTLE_TTL_MS } })
  async main(
    @Query('client_id') clientId: string,
    @Query('redirect_uri') redirectUri: string,
    @Query('response_type') responseType: string,
    @Query('state') state: string,
    @Query('scope') scope: string | undefined,
    @Query('code_challenge') codeChallenge: string,
    @Query('code_challenge_method') codeChallengeMethod: string,
    @Req() req: Request,
    @Res() res: Response,
  ): Promise<void> {
    // Extract the hub_session cookie accountId (null if absent or invalid).
    const rawCookie = parseHubSessionCookie(req.headers['cookie']);
    const accountId = rawCookie ? this.sessionService.verifyHubSession(rawCookie) : null;

    const result = await this.handler.main({
      clientId: clientId ?? '',
      redirectUri: redirectUri ?? '',
      responseType: responseType ?? '',
      state: state ?? '',
      scope,
      codeChallenge: codeChallenge ?? '',
      codeChallengeMethod: codeChallengeMethod ?? '',
      accountId,
      authorizeUrl: req.originalUrl,
    });

    // @Res() without passthrough: true hands full response control to Express —
    // NestJS does NOT apply route-level @Header() metadata in this mode.
    // Set the RFC 6749 §5.1 cache-prevention header explicitly before redirecting.
    res.setHeader('Cache-Control', 'no-store');

    if (result.kind === 'code') {
      res.redirect((result as OAuthAuthorizeCodeResult).redirectUrl);
    } else {
      res.redirect((result as OAuthAuthorizeRedirectResult).signInUrl);
    }
  }
}

/**
 * Extracts the `hub_session` value from the Cookie request header string.
 * Returns null when absent. No cookie-parser middleware required.
 *
 * Exported for unit testing only — callers outside this file should prefer
 * the controller, which owns the full HTTP lifecycle.
 */
export function parseHubSessionCookie(cookieHeader: string | undefined): string | null {
  if (!cookieHeader) return null;
  const match = cookieHeader.match(/(?:^|;\s*)hub_session=([^;]+)/);
  if (!match) return null;
  try {
    return decodeURIComponent(match[1]);
  } catch {
    // A malformed percent-encoded value (e.g. 'abc%') causes decodeURIComponent to throw
    // URIError. Treat an unparseable cookie as absent — fail-closed: redirect to sign-in
    // rather than surfacing a 500.
    return null;
  }
}
