import { OAuthHubSessionService } from '@app/o-auth/flows/application/session/o-auth-hub-session.service';
import {
  Body,
  Controller,
  Headers,
  Post,
  Res,
  UnauthorizedException,
  UseGuards,
} from '@nestjs/common';
import { Throttle, ThrottlerGuard } from '@nestjs/throttler';
import type { Response } from 'express';

const THROTTLE_LIMIT = Number(process.env.THROTTLE_OAUTH_SESSION_LIMIT ?? 10);
const THROTTLE_TTL_MS = Number(process.env.THROTTLE_OAUTH_SESSION_TTL_MS ?? 60_000);

/**
 * REST endpoint `POST /api/o-auth/session` — hub session establishment (D2).
 *
 * The frontend posts the access token from the Password grant. This endpoint verifies the
 * token (RS256 signature, aci claim) then sets the hub_session httpOnly cookie. After this,
 * the frontend navigates back to the continue URL (/authorize) which will find the session.
 *
 * Token sources accepted (D2 — NEVER in URL):
 *   1. JSON body `{ "access_token": "<jwt>" }` (preferred).
 *   2. Authorization: Bearer <jwt> header.
 *
 * Public endpoint — no @Auth decorator. The token IS the authentication proof.
 */
@Controller('o-auth')
export class OAuthSessionController {
  constructor(private readonly sessionService: OAuthHubSessionService) {}

  @Post('session')
  @UseGuards(ThrottlerGuard)
  @Throttle({ default: { limit: THROTTLE_LIMIT, ttl: THROTTLE_TTL_MS } })
  establish(
    @Body() body: Record<string, unknown>,
    @Headers('authorization') authorization: string | undefined,
    @Res() res: Response,
  ): void {
    const accessToken = resolveAccessToken(body, authorization);
    if (!accessToken) {
      throw new UnauthorizedException({ error: 'missing_token', statusCode: 401 });
    }

    const accountId = this.sessionService.verifyAccessTokenAndGetAccountId(accessToken);
    if (!accountId) {
      throw new UnauthorizedException({ error: 'invalid_token', statusCode: 401 });
    }

    // @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 headers explicitly before sending.
    res.setHeader('Cache-Control', 'no-store');
    res.setHeader('Pragma', 'no-cache');

    this.sessionService.createSessionCookie(res, accountId);
    res.status(204).send();
  }
}

/**
 * Resolves the access token from the request body or Authorization header.
 *
 * Priority: body `access_token` (preferred per D2) > Authorization Bearer header.
 * Returns null when neither source yields a non-empty token.
 *
 * Exported for unit testing only — callers outside this file should prefer
 * the controller, which owns the full HTTP lifecycle.
 */
export function resolveAccessToken(
  body: Record<string, unknown>,
  authorization: string | undefined,
): string | null {
  if (typeof body.access_token === 'string' && body.access_token) {
    return body.access_token;
  }
  if (authorization?.startsWith('Bearer ')) {
    const token = authorization.slice(7).trim();
    return token || null;
  }
  return null;
}
