import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { Prisma, IiosRouteBinding, IiosRouteDecision, IiosRouteMode, IiosOutputFormat } from '@prisma/client';
import { CloudEvent, IIOS_EVENTS } from '@insignia/iios-contracts';
import { currentTraceId, newTraceId, toTraceparent } from '../observability/trace-context';
import { recordAudit } from '../observability/audit';
import { PrismaService } from '../prisma/prisma.service';
import { RouteSimulator, type OriginRef } from './route-simulator';
import { OutboundService } from '../adapters/outbound.service';
import { ActorResolver, type MessagePrincipal } from '../identity/actor.resolver';

export interface CreateBindingInput {
  originChannelType: string;
  originRef?: string;
  destinationChannelType: string;
  destinationRef?: string;
  restrictionProfile?: string;
  mode?: IiosRouteMode;
  outputFormat?: IiosOutputFormat;
  frequency?: string;
  includeAttachments?: boolean;
  requiresReview?: boolean;
  enabled?: boolean;
}

@Injectable()
export class RouteService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly simulator: RouteSimulator,
    private readonly outbound: OutboundService,
    private readonly actors: ActorResolver,
  ) {}

  async createBinding(principal: MessagePrincipal, input: CreateBindingInput) {
    const scope = await this.actors.resolveScope(principal);
    return this.prisma.iiosRouteBinding.create({ data: { ...input, scopeId: scope.id } });
  }

  async listBindings(principal: MessagePrincipal) {
    const scope = await this.actors.resolveScope(principal);
    return this.prisma.iiosRouteBinding.findMany({ where: { scopeId: scope.id }, orderBy: { createdAt: 'desc' } });
  }

  simulate(interactionId: string, origin: OriginRef) {
    return this.simulator.simulate(interactionId, origin);
  }

  async listDecisions(principal: MessagePrincipal, opts: { state?: string } = {}) {
    const scope = await this.actors.findScope(principal);
    if (!scope) return [];
    return this.prisma.iiosRouteDecision.findMany({
      where: {
        routeBinding: { scopeId: scope.id },
        ...(opts.state ? { decisionState: opts.state as IiosRouteDecision['decisionState'] } : {}),
      },
      orderBy: { createdAt: 'desc' },
      take: 100,
      include: { routeBinding: true },
    });
  }

  async approve(decisionId: string, principal: MessagePrincipal) {
    const decision = await this.prisma.iiosRouteDecision.findUnique({ where: { id: decisionId }, include: { routeBinding: true } });
    if (!decision) throw new NotFoundException('route decision not found');
    await this.actors.assertOwns(principal, decision.routeBinding.scopeId); // tenant fence (KG-02)
    if (decision.decisionState === 'DENY' || decision.decisionState === 'SUPPRESS') {
      throw new BadRequestException(`cannot approve a ${decision.decisionState} decision`);
    }
    const actor = await this.actors.resolveActor(decision.routeBinding.scopeId, principal);
    const updated = await this.prisma.iiosRouteDecision.update({
      where: { id: decisionId },
      data: { decisionState: 'ALLOW', decidedByActorId: actor.id },
    });
    await this.emitApproved(decision.routeBinding.scopeId, decisionId);
    await recordAudit(this.prisma, { action: 'route.decision.approved', resourceType: 'route_decision', resourceId: decisionId, scopeId: decision.routeBinding.scopeId, actorRefId: actor.id });
    await this.execute(updated, decision.routeBinding);
    return this.prisma.iiosRouteDecision.findUniqueOrThrow({ where: { id: decisionId } });
  }

  async deny(decisionId: string, principal: MessagePrincipal) {
    const decision = await this.prisma.iiosRouteDecision.findUnique({ where: { id: decisionId }, include: { routeBinding: true } });
    if (!decision) throw new NotFoundException('route decision not found');
    await this.actors.assertOwns(principal, decision.routeBinding.scopeId); // tenant fence (KG-02)
    const actor = await this.actors.resolveActor(decision.routeBinding.scopeId, principal);
    const updated = await this.prisma.iiosRouteDecision.update({
      where: { id: decisionId },
      data: { decisionState: 'DENY', decidedByActorId: actor.id },
    });
    await recordAudit(this.prisma, { action: 'route.decision.denied', resourceType: 'route_decision', resourceId: decisionId, scopeId: decision.routeBinding.scopeId, actorRefId: actor.id });
    return updated;
  }

  /** Dispatch an ALLOW decision to the P5 sandbox (no real network). Idempotent. */
  async execute(decision: IiosRouteDecision, binding: IiosRouteBinding): Promise<void> {
    if (decision.executed || decision.decisionState !== 'ALLOW') return;
    const preview = decision.previewPayload as { text?: string };
    const cmd = await this.outbound.send(
      binding.destinationChannelType,
      binding.destinationRef ?? 'default',
      { text: preview.text ?? '' },
      `route:${decision.id}`,
      binding.scopeId,
      'route_forward',
    );
    await this.prisma.iiosRouteDecision.update({ where: { id: decision.id }, data: { executed: true, executedCommandId: cmd.id } });
  }

  /** Digest window MVP: batch this binding's un-executed ALLOW digest decisions into one forward. */
  async flushDigest(bindingId: string): Promise<{ forwarded: number }> {
    const binding = await this.prisma.iiosRouteBinding.findUniqueOrThrow({ where: { id: bindingId } });
    const pending = await this.prisma.iiosRouteDecision.findMany({
      where: { routeBindingId: bindingId, outputFormat: 'DIGEST', decisionState: 'ALLOW', executed: false },
      orderBy: { createdAt: 'asc' },
    });
    if (pending.length === 0) return { forwarded: 0 };
    const digest = pending.map((d) => (d.previewPayload as { text?: string }).text ?? '').join('\n');
    const cmd = await this.outbound.send(binding.destinationChannelType, binding.destinationRef ?? 'default', { text: `[digest x${pending.length}]\n${digest}` }, `digest:${bindingId}:${pending[pending.length - 1]!.id}`);
    await this.prisma.iiosRouteDecision.updateMany({
      where: { id: { in: pending.map((d) => d.id) } },
      data: { executed: true, executedCommandId: cmd.id },
    });
    return { forwarded: pending.length };
  }

  private async emitApproved(scopeId: string, decisionId: string): Promise<void> {
    const tid = currentTraceId() ?? newTraceId();
    const event: CloudEvent = {
      specversion: '1.0',
      id: `evt_routeapproved_${decisionId}`,
      type: IIOS_EVENTS.routeApproved,
      source: `iios/routing/${scopeId}`,
      subject: `route_decision/${decisionId}`,
      time: new Date().toISOString(),
      datacontenttype: 'application/json',
      traceparent: toTraceparent(tid),
      insignia: { scopeSnapshotId: scopeId, correlationId: tid, idempotencyKey: `routeapproved:${decisionId}`, dataClass: 'internal' },
      data: { decisionId },
    };
    await this.prisma.iiosOutboxEvent.create({
      data: {
        aggregateType: 'route_decision',
        aggregateId: decisionId,
        eventType: IIOS_EVENTS.routeApproved,
        cloudEvent: event as unknown as Prisma.InputJsonValue,
        partitionKey: `${scopeId}:${decisionId}`,
      },
    });
  }
}
