import { Inject, Injectable } from '@nestjs/common';
import type { CapabilityRequest, CapabilityResult, IiosPlatformPorts } from '@insignia/iios-contracts';
import { PLATFORM_PORTS } from '../platform/platform-ports';
import { decideOrThrow } from '../platform/fail-closed';
import { CapabilityProviderRegistry } from './capability.registry';

/**
 * The single governed egress boundary (P9). Every outbound action flows through
 * here: it fails closed on policy (nothing sent unless explicitly allowed),
 * enforces the policy's obligations BEFORE a provider is ever called, then routes
 * to the channel's provider. It is the concrete realization of the `capability`
 * platform port — a real broker replacing the permissive P1 stub.
 */
@Injectable()
export class CapabilityBroker {
  constructor(
    @Inject(PLATFORM_PORTS) private readonly ports: IiosPlatformPorts,
    private readonly registry: CapabilityProviderRegistry,
  ) {}

  async invoke(req: CapabilityRequest): Promise<CapabilityResult> {
    // 1. Fail-closed policy gate. Deny/timeout ⇒ PolicyDeniedError, nothing sent.
    const decision = await decideOrThrow(this.ports, {
      action: `iios.capability.${req.capability}`,
      scopeId: req.scopeId,
      channelType: req.channelType,
      target: req.target,
    });

    // 2. Enforce policy obligations BEFORE touching a provider.
    for (const ob of decision.obligations) {
      if (ob.kind === 'DENY_OUTBOUND' || ob.kind === 'REVIEW') {
        return { providerRef: 'blocked', outcome: 'BLOCKED', errorCode: ob.kind };
      }
    }

    // 3. Consent gate (CMP). Authorization (OPA) is not consent — a DENY here means the
    //    recipient did not consent to this purpose, so nothing is sent. Checked at send
    //    time, not enqueue time. NOT_REQUIRED/ALLOW proceed; ALLOW carries a receipt.
    const consent = await this.ports.cmp.checkPurpose({
      purpose: req.purpose ?? 'outbound_message',
      scopeId: req.scopeId,
      subject: req.target,
    });
    if (consent.status === 'DENY') {
      return { providerRef: 'blocked', outcome: 'BLOCKED', errorCode: 'CONSENT_DENIED' };
    }

    const payload = this.applyMasks(req.payload, decision.obligations);

    // 4. Route to the channel's provider (unknown channel fails closed in the registry).
    const provider = this.registry.forChannel(req.channelType);
    const result = await provider.send({ ...req, payload });

    // 5. Normalize the provider's outcome + record the consent receipt for provenance.
    const outcome = result.outcome === 'SENT' ? 'SENT' : result.outcome === 'RATE_LIMITED' ? 'RATE_LIMITED' : 'FAILED';
    return { providerRef: result.providerRef, outcome, errorCode: result.errorCode, latencyMs: result.latencyMs, consentReceiptRef: consent.receiptRef };
  }

  private applyMasks(
    payload: Record<string, unknown>,
    obligations: { kind: string; target?: string }[],
  ): Record<string, unknown> {
    const masks = obligations.filter((o) => (o.kind === 'MASK' || o.kind === 'REDACT') && o.target);
    if (masks.length === 0) return payload;
    const out = { ...payload };
    for (const m of masks) out[m.target as string] = '[REDACTED]';
    return out;
  }
}
