import { randomUUID } from 'node:crypto';
import { BadRequestException, Body, Controller, ForbiddenException, Get, Headers, Param, Post, UnauthorizedException } from '@nestjs/common';
import jwt from 'jsonwebtoken';
import { IIOS_EVENTS } from '@insignia/iios-contracts';
import { hmacSign } from '@insignia/iios-adapter-sdk';
import { InboundService } from '../adapters/inbound.service';
import { AdapterRegistry } from '../adapters/adapter.registry';
import { PrismaService } from '../prisma/prisma.service';
import { ActorResolver, type MessagePrincipal } from '../identity/actor.resolver';
import { SessionVerifier } from '../platform/session.verifier';
import { RetentionService } from '../retention/retention.service';

/**
 * Dev-only helpers (gated by IIOS_DEV_TOKENS=1). Never enable in production.
 */
@Controller('v1/dev')
export class DevController {
  constructor(
    private readonly inbound: InboundService,
    private readonly registry: AdapterRegistry,
    private readonly prisma: PrismaService,
    private readonly actors: ActorResolver,
    private readonly session: SessionVerifier,
    private readonly retention: RetentionService,
  ) {}

  @Post('token')
  token(@Body() body: { appId: string; userId: string; name?: string; orgId?: string }): { token: string } {
    this.assertDev();
    return { token: this.signToken(body.appId, body.userId, body.name, body.orgId) };
  }

  /**
   * Dev IdP: a credentialed login that mints the SAME JWT claims a real IdP / Session
   * Broker would issue (sub/name/appId/orgId). Credentials come from DEV_USERS (JSON
   * `{username: password}`; defaults to a few demo users). SessionVerifier is the stable
   * verify seam — swapping in a real IdP means issuing these same claims, nothing else.
   */
  @Post('login')
  login(@Body() body: { username: string; password: string; appId?: string }): { token: string; userId: string } {
    this.assertDev();
    const appId = body.appId ?? 'portal-demo';
    let users: Record<string, string>;
    try {
      users = JSON.parse(process.env.DEV_USERS ?? '{"alice":"alice","bob":"bob","carol":"carol"}');
    } catch {
      users = {};
    }
    const expected = users[body.username];
    if (expected === undefined || expected !== body.password) throw new UnauthorizedException('invalid username or password');
    return { token: this.signToken(appId, body.username, body.username), userId: body.username };
  }

  /** Dev directory: the known dev usernames (from DEV_USERS). A real app validates
   *  add-by-username against its user store / the MDM port; this stands in for it. */
  @Get('users')
  users(): { users: string[] } {
    this.assertDev();
    let map: Record<string, string>;
    try {
      map = JSON.parse(process.env.DEV_USERS ?? '{"alice":"alice","bob":"bob","carol":"carol"}');
    } catch {
      map = {};
    }
    return { users: Object.keys(map) };
  }

  private signToken(appId: string, userId: string, name?: string, orgId?: string): string {
    let secrets: Record<string, string>;
    try {
      secrets = JSON.parse(process.env.APP_SECRETS ?? '{}');
    } catch {
      secrets = {};
    }
    const secret = secrets[appId];
    if (!secret) throw new BadRequestException(`unknown app: ${appId}`);
    return jwt.sign(
      { sub: userId, name: name ?? userId, appId, orgId: orgId ?? `org_${appId}` },
      secret,
      { algorithm: 'HS256', expiresIn: '2h' },
    );
  }

  /** Server signs a sample provider payload and feeds it to the inbound pipeline. */
  @Post('webhook/:channelType')
  async injectWebhook(@Param('channelType') channelType: string, @Body() body?: { from?: string; text?: string }) {
    this.assertDev();
    const reg = this.registry.get(channelType);
    if (!reg) throw new BadRequestException(`unknown channel type: ${channelType}`);
    const payload = {
      eventId: `dev-${randomUUID()}`,
      from: body?.from ?? 'sim@customer',
      displayName: 'Sim Customer',
      threadRef: `sim-${randomUUID().slice(0, 8)}`,
      subject: 'Simulated inbound',
      text: body?.text ?? 'Hello from a simulated provider',
    };
    const raw = JSON.stringify(payload);
    return this.inbound.receive(channelType, raw, {
      'x-iios-signature': hmacSign(raw, reg.config.secret),
      'content-type': 'application/json',
    });
  }

  /**
   * Inject a poison chaos event into the outbox. The relay publishes it, the
   * ChaosConsumer throws on first delivery → it is dead-lettered under the caller's
   * scope, then `POST /v1/dlq/:id/replay` resolves it. Drives the DLQ smoke.
   */
  @Post('chaos/poison')
  async poison(@Headers('authorization') auth?: string): Promise<{ eventId: string; scopeId: string }> {
    this.assertDev();
    const principal = this.principal(auth);
    const scope = await this.actors.resolveScope(principal); // find-or-create so GET /v1/dlq matches
    const id = `evt_chaos_${randomUUID()}`;
    const cloudEvent = {
      specversion: '1.0',
      id,
      type: IIOS_EVENTS.devChaos,
      source: `iios/dev/${principal.appId}`,
      subject: `chaos/${id}`,
      time: new Date().toISOString(),
      datacontenttype: 'application/json',
      insignia: { scopeSnapshotId: scope.id, idempotencyKey: id, dataClass: 'internal' },
      data: { poison: true },
    };
    await this.prisma.iiosOutboxEvent.create({
      data: {
        aggregateType: 'dev-chaos',
        aggregateId: id,
        eventType: IIOS_EVENTS.devChaos,
        cloudEvent: cloudEvent as unknown as object,
        partitionKey: `${principal.orgId}:${principal.appId}:chaos`,
      },
    });
    return { eventId: id, scopeId: scope.id };
  }

  /** Run the retention sweep on demand (drives the retention smoke). */
  @Post('retention/sweep')
  async retentionSweep() {
    this.assertDev();
    return this.retention.sweep();
  }

  private principal(authorization?: string): MessagePrincipal {
    const token = (authorization ?? '').replace(/^Bearer\s+/i, '');
    if (!token) throw new BadRequestException('Authorization bearer token is required');
    return this.session.verify(token);
  }

  private assertDev(): void {
    if (process.env.IIOS_DEV_TOKENS !== '1') throw new ForbiddenException('dev endpoints disabled');
  }
}
