import { Controller, Get, Inject, Query, Req, Res, UnauthorizedException } from '@nestjs/common';
import { ApiCookieAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import type { Request, Response } from 'express';
import { Logger } from 'nestjs-pino';
import { AuditWriter } from '../audit/audit.service';
import {
  PRE_AUTH_COOKIE_NAME,
  clearPreAuthCookieOptions,
  preAuthCookieOptions,
} from '../auth/auth.cookie';
import { AuthCodeFlowException, type AuthCodeFlowError, authErrorCode } from '../auth/auth.errors';
import { AuthService, type PreAuthPayload } from '../auth/auth.service';
import { ENTRA_CONFIG, type EntraConfig } from '../auth/entra-config.token';
import { SessionEstablisher } from '../auth/session-establisher.service';
import { csrfCookieName } from '../security/csrf-cookie';
import { adminSessionCookieName } from '../session/admin-session-cookie';

/**
 * `/api/admin/auth/*` — admin-portal OIDC routes per ADR-0020.
 *
 * Structurally identical to the user-portal `AuthController`:
 * `GET /login` 302s to Entra with PKCE + state in a signed cookie,
 * `GET /callback` exchanges the code for tokens, establishes the
 * admin session, and redirects to the admin SPA. `GET /me` reads
 * the admin session back. `GET /logout` destroys the admin session
 * and redirects to Entra's RP-initiated logout.
 *
 * The key difference is *which* `req.session` we're looking at: the
 * session middleware mounted on `/api/admin/*` in `main.ts` resolves
 * to the admin-session Redis namespace (`session:admin:*`) and uses
 * the `__Host-portal_admin_session` cookie, distinct from
 * `__Host-portal_session` used by the user portal. Per ADR-0020
 * §"Sessions — distinct from `portal-shell`": signing in to one
 * surface does NOT sign in to the other.
 *
 * Routes here are **deliberately not** guarded with `@RequireAdmin`
 * or `@RequireMfa`. The whole point of the login flow is to *create*
 * the session that those guards check against. Sub-controllers
 * (admin business routes) layer the guards on top.
 */
@ApiTags('auth (admin portal)')
@Controller('admin/auth')
export class AdminAuthController {
  constructor(
    private readonly authService: AuthService,
    private readonly logger: Logger,
    @Inject(ENTRA_CONFIG) private readonly entra: EntraConfig,
    private readonly audit: AuditWriter,
    private readonly sessionEstablisher: SessionEstablisher,
  ) {}

  @ApiOperation({ summary: 'Start the admin OIDC Auth Code + PKCE flow (302 to Entra)' })
  @Get('login')
  async login(@Res() res: Response): Promise<void> {
    const { authUrl, preAuthPayload } = await this.authService.beginAuthCodeFlow(
      this.entra.adminRedirectUri,
    );
    res.cookie(PRE_AUTH_COOKIE_NAME, JSON.stringify(preAuthPayload), preAuthCookieOptions());
    res.redirect(302, authUrl);
  }

  @ApiOperation({
    summary: 'Entra callback for the admin surface — establishes the admin session',
  })
  @Get('callback')
  async callback(
    @Req() req: Request,
    @Res() res: Response,
    @Query('code') code?: string,
    @Query('state') state?: string,
    @Query('error') entraError?: string,
    @Query('error_description') entraErrorDescription?: string,
  ): Promise<void> {
    res.clearCookie(PRE_AUTH_COOKIE_NAME, clearPreAuthCookieOptions());

    if (entraError) {
      this.logger.warn(
        {
          event: 'auth.entra_error',
          surface: 'admin',
          entraError,
          entraErrorDescription,
        },
        'AdminAuthCallback',
      );
      await this.audit.signInFailed({
        failureKind: 'entra-error',
        payload: { surface: 'admin', entraError, entraErrorDescription },
      });
      return this.redirectWithError(res, 'token-exchange-failed');
    }

    if (typeof code !== 'string' || typeof state !== 'string') {
      await this.audit.signInFailed({
        failureKind: 'missing-code-or-state',
        payload: { surface: 'admin' },
      });
      return this.redirectWithError(res, 'token-exchange-failed');
    }

    const preAuth = readPreAuthCookie(req);
    if (!preAuth) {
      this.logger.warn({ event: 'auth.no_pre_auth_cookie', surface: 'admin' }, 'AdminAuthCallback');
      await this.audit.signInFailed({
        failureKind: 'no-pre-auth-cookie',
        payload: { surface: 'admin' },
      });
      return this.redirectWithError(res, 'flow-expired');
    }

    try {
      const user = await this.authService.completeAuthCodeFlow(
        code,
        state,
        preAuth,
        this.entra.adminRedirectUri,
      );
      await this.sessionEstablisher.establish({ user, req, res, surface: 'admin' });
      res.redirect(302, this.entra.adminPostLogoutRedirectUri);
    } catch (err) {
      if (err instanceof AuthCodeFlowException) {
        this.logger.warn(
          { event: 'auth.flow_error', surface: 'admin', failure: err.failure },
          'AdminAuthCallback',
        );
        await this.audit.signInFailed({
          failureKind: err.failure.kind,
          payload: { surface: 'admin' },
        });
        return this.redirectWithError(res, err.failure.kind);
      }
      throw err;
    }
  }

  @ApiOperation({ summary: 'Current admin session payload (includes `roles`)' })
  @ApiCookieAuth('portal_admin_session')
  @Get('me')
  me(@Req() req: Request, @Res() res: Response): void {
    const user = req.session.user;
    if (!user) {
      throw new UnauthorizedException({
        code: 'unauthenticated',
        message: 'Unauthenticated',
      });
    }
    res.json({
      oid: user.oid,
      tid: user.tid,
      username: user.username,
      displayName: user.displayName,
      // Admins benefit from seeing their role assignment in the
      // SPA — drives conditional UI for super-admin features later.
      // Other surfaces (`/api/auth/me`) still omit it.
      roles: user.roles,
    });
  }

  @ApiOperation({ summary: 'RP-initiated admin logout — destroys admin session' })
  @ApiCookieAuth('portal_admin_session')
  @Get('logout')
  async logout(@Req() req: Request, @Res() res: Response): Promise<void> {
    const user = req.session.user;
    const wasAuthenticated = Boolean(user);
    const logoutUrl = this.authService.buildLogoutUrl(this.entra.adminPostLogoutRedirectUri);

    await this.sessionEstablisher.destroy({ actor: user, req });

    res.clearCookie(adminSessionCookieName(), { path: '/' });
    res.clearCookie(csrfCookieName(), { path: '/' });
    this.logger.log(
      { event: 'auth.signed_out', surface: 'admin', wasAuthenticated },
      'AdminAuthLogout',
    );
    res.redirect(302, logoutUrl);
  }

  private redirectWithError(res: Response, kind: AuthCodeFlowError['kind']): void {
    const url = new URL(this.entra.adminPostLogoutRedirectUri);
    url.searchParams.set('auth_error', authErrorCode({ kind } as AuthCodeFlowError));
    res.redirect(302, url.toString());
  }
}

function readPreAuthCookie(req: Request): PreAuthPayload | null {
  const raw = (req.signedCookies as Record<string, unknown>)[PRE_AUTH_COOKIE_NAME];
  if (typeof raw !== 'string') {
    return null;
  }
  try {
    return JSON.parse(raw) as PreAuthPayload;
  } catch {
    return null;
  }
}
