/**
 * FederationController — admin REST API for federation management (FED-M2-08).
 *
 * Routes (all under /api/admin/federation, all require AdminGuard):
 *
 *   Grant management:
 *     POST   /api/admin/federation/grants
 *     GET    /api/admin/federation/grants
 *     GET    /api/admin/federation/grants/:id
 *     PATCH  /api/admin/federation/grants/:id/revoke
 *     POST   /api/admin/federation/grants/:id/tokens
 *
 *   Peer management:
 *     GET    /api/admin/federation/peers
 *     POST   /api/admin/federation/peers/keypair
 *     PATCH  /api/admin/federation/peers/:id/cert
 *
 * NOTE: The enrollment REDEMPTION endpoint (POST /api/federation/enrollment/:token)
 * is handled by EnrollmentController — not duplicated here.
 */

import {
  Body,
  Controller,
  Get,
  HttpCode,
  HttpStatus,
  Inject,
  NotFoundException,
  Param,
  Patch,
  Post,
  Query,
  UseGuards,
} from '@nestjs/common';
import { webcrypto } from 'node:crypto';
import { X509Certificate } from 'node:crypto';
import { Pkcs10CertificateRequestGenerator } from '@peculiar/x509';
import { type Db, eq, federationPeers } from '@mosaicstack/db';
import { DB } from '../database/database.module.js';
import { AdminGuard } from '../admin/admin.guard.js';
import { GrantsService } from './grants.service.js';
import { EnrollmentService } from './enrollment.service.js';
import { sealClientKey } from './peer-key.util.js';
import { CreateGrantDto, ListGrantsDto } from './grants.dto.js';
import {
  CreatePeerKeypairDto,
  GenerateEnrollmentTokenDto,
  RevokeGrantBodyDto,
  StorePeerCertDto,
} from './federation-admin.dto.js';

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/**
 * Convert an ArrayBuffer to a Base64 string (for PEM encoding).
 */
function arrayBufferToBase64(buf: ArrayBuffer): string {
  const bytes = new Uint8Array(buf);
  let binary = '';
  for (const b of bytes) {
    binary += String.fromCharCode(b);
  }
  return Buffer.from(binary, 'binary').toString('base64');
}

/**
 * Wrap a Base64 string in PEM armour.
 */
function toPem(label: string, b64: string): string {
  const lines = b64.match(/.{1,64}/g) ?? [];
  return `-----BEGIN ${label}-----\n${lines.join('\n')}\n-----END ${label}-----\n`;
}

// ---------------------------------------------------------------------------
// Controller
// ---------------------------------------------------------------------------

@Controller('api/admin/federation')
@UseGuards(AdminGuard)
export class FederationController {
  constructor(
    @Inject(DB) private readonly db: Db,
    @Inject(GrantsService) private readonly grantsService: GrantsService,
    @Inject(EnrollmentService) private readonly enrollmentService: EnrollmentService,
  ) {}

  // ─── Grant management ────────────────────────────────────────────────────

  /**
   * POST /api/admin/federation/grants
   * Create a new grant in pending state.
   */
  @Post('grants')
  @HttpCode(HttpStatus.CREATED)
  async createGrant(@Body() body: CreateGrantDto) {
    return this.grantsService.createGrant(body);
  }

  /**
   * GET /api/admin/federation/grants
   * List grants with optional filters.
   */
  @Get('grants')
  async listGrants(@Query() query: ListGrantsDto) {
    return this.grantsService.listGrants(query);
  }

  /**
   * GET /api/admin/federation/grants/:id
   * Get a single grant by ID.
   */
  @Get('grants/:id')
  async getGrant(@Param('id') id: string) {
    return this.grantsService.getGrant(id);
  }

  /**
   * PATCH /api/admin/federation/grants/:id/revoke
   * Revoke an active grant.
   */
  @Patch('grants/:id/revoke')
  async revokeGrant(@Param('id') id: string, @Body() body: RevokeGrantBodyDto) {
    return this.grantsService.revokeGrant(id, body.reason);
  }

  /**
   * POST /api/admin/federation/grants/:id/tokens
   * Generate a single-use enrollment token for a pending grant.
   * Returns the token plus an enrollmentUrl the operator shares out-of-band.
   */
  @Post('grants/:id/tokens')
  @HttpCode(HttpStatus.CREATED)
  async generateToken(@Param('id') id: string, @Body() body: GenerateEnrollmentTokenDto) {
    const grant = await this.grantsService.getGrant(id);

    const result = await this.enrollmentService.createToken({
      grantId: id,
      peerId: grant.peerId,
      ttlSeconds: body.ttlSeconds ?? 900,
    });

    const baseUrl = process.env['BETTER_AUTH_URL'] ?? 'http://localhost:14242';
    const enrollmentUrl = `${baseUrl}/api/federation/enrollment/${result.token}`;

    return {
      token: result.token,
      expiresAt: result.expiresAt,
      enrollmentUrl,
    };
  }

  // ─── Peer management ─────────────────────────────────────────────────────

  /**
   * GET /api/admin/federation/peers
   * List all federation peer rows.
   */
  @Get('peers')
  async listPeers() {
    return this.db.select().from(federationPeers).orderBy(federationPeers.commonName);
  }

  /**
   * POST /api/admin/federation/peers/keypair
   * Generate a new peer entry with EC P-256 key pair and a PKCS#10 CSR.
   *
   * Flow:
   *  1. Generate EC P-256 key pair via webcrypto
   *  2. Generate a self-signed CSR via @peculiar/x509
   *  3. Export private key as PEM
   *  4. sealClientKey(privatePem) → sealed blob
   *  5. Insert pending peer row
   *  6. Return { peerId, csrPem }
   */
  @Post('peers/keypair')
  @HttpCode(HttpStatus.CREATED)
  async createPeerKeypair(@Body() body: CreatePeerKeypairDto) {
    // 1. Generate EC P-256 key pair via Web Crypto
    const keyPair = await webcrypto.subtle.generateKey(
      { name: 'ECDSA', namedCurve: 'P-256' },
      true, // extractable
      ['sign', 'verify'],
    );

    // 2. Generate PKCS#10 CSR
    const csr = await Pkcs10CertificateRequestGenerator.create({
      name: `CN=${body.commonName}`,
      keys: keyPair,
      signingAlgorithm: { name: 'ECDSA', hash: 'SHA-256' },
    });

    const csrPem = csr.toString('pem');

    // 3. Export private key as PKCS#8 PEM
    const pkcs8Der = await webcrypto.subtle.exportKey('pkcs8', keyPair.privateKey);
    const privatePem = toPem('PRIVATE KEY', arrayBufferToBase64(pkcs8Der));

    // 4. Seal the private key
    const sealed = sealClientKey(privatePem);

    // 5. Insert pending peer row
    const [peer] = await this.db
      .insert(federationPeers)
      .values({
        commonName: body.commonName,
        displayName: body.displayName,
        certPem: '',
        certSerial: 'pending',
        certNotAfter: new Date(0),
        clientKeyPem: sealed,
        state: 'pending',
        endpointUrl: body.endpointUrl,
      })
      .returning();

    return {
      peerId: peer!.id,
      csrPem,
    };
  }

  /**
   * PATCH /api/admin/federation/peers/:id/cert
   * Store a signed certificate after enrollment completes.
   *
   * Flow:
   *  1. Parse the cert to extract serial and notAfter
   *  2. Update the peer row with cert data + state='active'
   *  3. Return the updated peer row
   */
  @Patch('peers/:id/cert')
  async storePeerCert(@Param('id') id: string, @Body() body: StorePeerCertDto) {
    // Ensure peer exists
    const [existing] = await this.db
      .select({ id: federationPeers.id })
      .from(federationPeers)
      .where(eq(federationPeers.id, id))
      .limit(1);

    if (!existing) {
      throw new NotFoundException(`Peer ${id} not found`);
    }

    // 1. Parse cert
    const x509 = new X509Certificate(body.certPem);
    const certSerial = x509.serialNumber;
    const certNotAfter = new Date(x509.validTo);

    // 2. Update peer
    const [updated] = await this.db
      .update(federationPeers)
      .set({
        certPem: body.certPem,
        certSerial,
        certNotAfter,
        state: 'active',
      })
      .where(eq(federationPeers.id, id))
      .returning();

    return updated;
  }
}
