import { randomUUID } from 'node:crypto';
import { Injectable, NotFoundException } from '@nestjs/common';
import { Prisma, IiosRouteBinding } from '@prisma/client';
import { CloudEvent, IIOS_EVENTS } from '@insignia/iios-contracts';
import { PrismaService } from '../prisma/prisma.service';
import { RuleEngine } from './rule-engine';

export interface OriginRef {
  channelType: string;
  ref?: string;
}

/**
 * Preview-first route simulator (P6). Produces a decision + rendered preview per
 * enabled binding for an origin — and NEVER sends. Deterministic + replayable
 * (re-simulating yields the same decisions). Persists decisions so an admin can
 * approve by id.
 */
@Injectable()
export class RouteSimulator {
  constructor(
    private readonly prisma: PrismaService,
    private readonly rules: RuleEngine,
  ) {}

  async simulate(interactionId: string, origin: OriginRef, opts?: { simulationId?: string }) {
    const interaction = await this.prisma.iiosInteraction.findUnique({
      where: { id: interactionId },
      include: { parts: { orderBy: { partIndex: 'asc' } } },
    });
    if (!interaction) throw new NotFoundException('interaction not found');

    const text = interaction.parts.find((p) => p.kind === 'TEXT')?.bodyText ?? '';
    const hasAttachment = interaction.parts.some((p) => !!p.contentRef);
    const ruleFlags = this.rules.flag(text);
    await this.persistFlags(interactionId, ruleFlags);

    // Union deterministic RULE flags with any persisted flags — including
    // proposedBy:'AI' from the P7 layer. AI flags therefore influence routing
    // exactly like rule flags: they can only push toward REVIEW/DENY (KG-05).
    const persisted = await this.prisma.iiosModerationFlag.findMany({ where: { interactionId } });
    const flagTypes = Array.from(new Set([...ruleFlags.map((f) => f.flagType), ...persisted.map((p) => p.flagType)]));

    // A P7 AI summary (if one was proposed) renders into SUMMARY/TRANSCRIPT/DIGEST previews.
    const aiSummary = await this.latestAiSummary(interactionId);

    const simulationId = opts?.simulationId ?? randomUUID();
    const bindings = await this.prisma.iiosRouteBinding.findMany({
      where: {
        scopeId: interaction.scopeId,
        enabled: true,
        originChannelType: origin.channelType,
        ...(origin.ref ? { originRef: origin.ref } : {}),
      },
    });

    const decisions = [];
    for (const b of bindings) {
      const { state, reasonCodes } = this.rules.decide(b, flagTypes);
      const previewPayload = this.render(b, text, hasAttachment, aiSummary);
      const decision = await this.prisma.iiosRouteDecision.upsert({
        where: { interactionId_routeBindingId: { interactionId, routeBindingId: b.id } },
        create: {
          interactionId,
          routeBindingId: b.id,
          decisionState: state,
          reasonCodes,
          previewPayload: previewPayload as Prisma.InputJsonValue,
          outputFormat: b.outputFormat,
          rulepackVersion: this.rules.rulepackVersion,
          simulationId,
        },
        update: { decisionState: state, reasonCodes, previewPayload: previewPayload as Prisma.InputJsonValue, simulationId },
      });
      decisions.push(decision);
    }

    // Trace event — a preview record, never a send.
    if (decisions.length > 0) {
      const event: CloudEvent = {
        specversion: '1.0',
        id: `evt_routesim_${simulationId}`,
        type: IIOS_EVENTS.routeDecisionPreviewed,
        source: `iios/routing/${interaction.scopeId}`,
        subject: `interaction/${interactionId}`,
        time: new Date().toISOString(),
        datacontenttype: 'application/json',
        insignia: { scopeSnapshotId: interaction.scopeId, correlationId: interaction.traceId ?? undefined, idempotencyKey: `routesim:${simulationId}`, dataClass: 'internal' },
        data: { simulationId, interactionId, decisions: decisions.map((d) => ({ id: d.id, state: d.decisionState })) },
      };
      await this.prisma.iiosOutboxEvent.create({
        data: {
          aggregateType: 'route_simulation',
          aggregateId: interactionId,
          eventType: IIOS_EVENTS.routeDecisionPreviewed,
          cloudEvent: event as unknown as Prisma.InputJsonValue,
          partitionKey: `${interaction.scopeId}:${interactionId}`,
        },
      });
    }

    return { simulationId, decisions };
  }

  private render(binding: IiosRouteBinding, text: string, hasAttachment: boolean, aiSummary?: string) {
    const attach = binding.includeAttachments && hasAttachment ? ' [+attachment]' : '';
    // SUMMARY/TRANSCRIPT/DIGEST use the P7 AI summary if one was proposed; else the
    // P6 deterministic stub. FORWARD/THREADED are verbatim. Still preview-only — no send.
    const rendered =
      binding.outputFormat === 'DIGEST'
        ? `[digest] ${aiSummary ?? text}`
        : binding.outputFormat === 'SUMMARY'
          ? (aiSummary ?? `[summary] ${text.slice(0, 60)}`)
          : binding.outputFormat === 'TRANSCRIPT'
            ? `[transcript] ${aiSummary ?? text}`
            : text; // FORWARD / THREADED
    return {
      format: binding.outputFormat,
      destinationChannelType: binding.destinationChannelType,
      destinationRef: binding.destinationRef,
      aiSourced: !!aiSummary && binding.outputFormat !== 'FORWARD' && binding.outputFormat !== 'THREADED',
      text: rendered + attach,
    };
  }

  private async latestAiSummary(interactionId: string): Promise<string | undefined> {
    const artifact = await this.prisma.iiosAiArtifact.findFirst({
      where: { artifactType: 'SUMMARY', job: { interactionId } },
      orderBy: { createdAt: 'desc' },
    });
    const content = artifact?.contentRef as { text?: string } | undefined;
    return content?.text;
  }

  private async persistFlags(interactionId: string, flags: RuleFlagLike[]): Promise<void> {
    if (flags.length === 0) return;
    const existing = await this.prisma.iiosModerationFlag.count({ where: { interactionId, proposedBy: 'RULE' } });
    if (existing > 0) return; // keep replay deterministic — flag once
    await this.prisma.iiosModerationFlag.createMany({
      data: flags.map((f) => ({ interactionId, flagType: f.flagType, proposedBy: 'RULE' })),
    });
  }
}

interface RuleFlagLike {
  flagType: string;
}
