import { createHash } from 'node:crypto';
import { Inject, Injectable, NotFoundException } from '@nestjs/common';
import { Prisma, IiosAiJobType } from '@prisma/client';
import {
  CloudEvent,
  IIOS_EVENTS,
  IiosPlatformPorts,
  InferencePort,
  InferenceResult,
} from '@insignia/iios-contracts';
import { currentTraceId, newTraceId, toTraceparent } from '../observability/trace-context';
import { recordAudit } from '../observability/audit';
import { PrismaService } from '../prisma/prisma.service';
import { PLATFORM_PORTS } from '../platform/platform-ports';
import { decideOrThrow } from '../platform/fail-closed';
import { ActorResolver, type MessagePrincipal } from '../identity/actor.resolver';
import { INFERENCE_PORT } from './inference.port';
import { AiBudgetGuard } from './ai-budget.guard';

export interface RunJobInput {
  interactionId: string;
  jobType: IiosAiJobType;
  /** Optional — the job is scoped by the interaction, so the auto-projector needs no principal. */
  principal?: MessagePrincipal;
  idempotencyKey?: string;
}

/**
 * The AI proposal layer (P7). Runs a scoped, purposed AI job that **only proposes**:
 * it fails closed through OPA, degrades on budget (KG-12), and persists a durable
 * artifact + claims + evidence + model-run with full provenance. It never writes a
 * route decision, sends a message, merges identity, or approves anything — a human
 * (or a deterministic P6 rule) is always the gate. CLASSIFY additionally proposes
 * `IiosModerationFlag(proposedBy:'AI')` which can only push routing toward
 * REVIEW/DENY, never ALLOW (KG-05).
 */
@Injectable()
export class AiJobService {
  constructor(
    private readonly prisma: PrismaService,
    @Inject(PLATFORM_PORTS) private readonly ports: IiosPlatformPorts,
    @Inject(INFERENCE_PORT) private readonly inference: InferencePort,
    private readonly budget: AiBudgetGuard,
    private readonly actors: ActorResolver,
  ) {}

  async runJob(input: RunJobInput) {
    const { interactionId, jobType } = input;
    const idempotencyKey = input.idempotencyKey ?? `ai:${jobType}:${interactionId}`;

    // Idempotent: a repeated key returns the existing job + its artifact — no duplicate work.
    const prior = await this.prisma.iiosAiJob.findUnique({
      where: { idempotencyKey },
      include: { artifacts: { include: { claims: true, evidence: true } } },
    });
    if (prior) return { job: prior, artifact: prior.artifacts[0] ?? null };

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

    const scopeId = interaction.scopeId;
    const text = interaction.parts.find((p) => p.kind === 'TEXT')?.bodyText ?? '';
    const purpose = `ai_${jobType.toLowerCase()}`;
    const inputScopeHash = createHash('sha256').update(`${scopeId}:${interactionId}`).digest('hex').slice(0, 16);
    const base = {
      scopeId,
      jobType,
      interactionId,
      inputWindowRef: interactionId,
      purpose,
      inputScopeHash,
      idempotencyKey,
      traceId: interaction.traceId ?? undefined,
    };

    // 1. Fail-closed authorization (Critics KG-03). Deny/timeout ⇒ FAILED job, no artifact.
    let policyDecisionRef: string;
    try {
      const decision = await decideOrThrow(this.ports, { action: 'iios.ai.infer', scopeId, jobType, purpose });
      policyDecisionRef = decision.decisionRef;
    } catch (err) {
      await this.prisma.iiosAiJob.create({ data: { ...base, status: 'FAILED', abstentionReason: 'POLICY_DENIED' } });
      throw err;
    }

    // 2. Budget governor (Critics KG-12). Exceeded ⇒ ABSTAINED job, no artifact (degrade mode).
    const budget = await this.budget.check(scopeId);
    if (!budget.ok) {
      const job = await this.prisma.iiosAiJob.create({
        data: { ...base, status: 'ABSTAINED', policyDecisionRef, abstentionReason: 'BUDGET_EXCEEDED' },
      });
      return { job, artifact: null };
    }

    // 3. Inference. A provider error ⇒ FAILED job, no artifact (fail closed on the model too).
    let result: InferenceResult;
    try {
      result = await this.inference.infer({ jobType, text, purpose, scopeId, interactionId });
    } catch {
      const job = await this.prisma.iiosAiJob.create({
        data: { ...base, status: 'FAILED', policyDecisionRef, abstentionReason: 'PROVIDER_ERROR' },
      });
      return { job, artifact: null };
    }

    // 4. Persist the proposal (job + model run + artifact + claims + evidence).
    const job = await this.prisma.iiosAiJob.create({ data: { ...base, status: 'DONE', policyDecisionRef } });
    const modelRun = await this.prisma.iiosAiModelRun.create({ data: { scopeId, ...result.modelRun } });
    const artifact = await this.prisma.iiosAiArtifact.create({
      data: {
        jobId: job.id,
        modelRunId: modelRun.id,
        artifactType: result.artifactType,
        confidence: result.confidence,
        contentRef: result.content as Prisma.InputJsonValue,
        explanationRef: result.explanation,
        abstentionReason: result.abstentionReason,
        claims: {
          create: result.claims.map((c) => ({
            claimType: c.claimType,
            claimJson: c.claimJson as Prisma.InputJsonValue,
            confidence: c.confidence,
          })),
        },
        evidence: {
          create: result.evidence.map((e) => ({
            sourceType: e.sourceType,
            sourceId: e.sourceId,
            spanRef: e.spanRef,
            relevanceScore: e.relevanceScore,
          })),
        },
      },
      include: { claims: true, evidence: true },
    });

    // CLASSIFY proposes AI moderation flags (the P6 seam). Once per (interaction, flagType)
    // for replay-safety. These feed rule-engine.decide() — REVIEW/DENY only, never ALLOW.
    if (jobType === 'CLASSIFY') {
      for (const claim of result.claims.filter((c) => c.claimType === 'MODERATION_FLAG')) {
        const flagType = String(claim.claimJson.flagType ?? '');
        if (!flagType) continue;
        const exists = await this.prisma.iiosModerationFlag.count({ where: { interactionId, flagType, proposedBy: 'AI' } });
        if (exists === 0) {
          await this.prisma.iiosModerationFlag.create({
            data: {
              interactionId,
              flagType,
              proposedBy: 'AI',
              confidence: claim.confidence,
              explanation: String(claim.claimJson.explanation ?? result.explanation ?? ''),
            },
          });
        }
      }
    }

    await this.emit(IIOS_EVENTS.aiArtifactProposed, scopeId, artifact.id, {
      jobId: job.id,
      artifactId: artifact.id,
      artifactType: artifact.artifactType,
      interactionId,
    });

    return { job, artifact };
  }

  async listArtifacts(principal: MessagePrincipal, opts: { interactionId?: string; status?: string } = {}) {
    const scope = await this.actors.findScope(principal);
    if (!scope) return [];
    return this.prisma.iiosAiArtifact.findMany({
      where: {
        job: { scopeId: scope.id, ...(opts.interactionId ? { interactionId: opts.interactionId } : {}) },
        ...(opts.status ? { status: opts.status as Prisma.EnumIiosAiArtifactStatusFilter } : {}),
      },
      orderBy: { createdAt: 'desc' },
      take: 100,
      include: { claims: true, evidence: true, modelRun: true, job: true },
    });
  }

  async getArtifact(id: string, principal: MessagePrincipal) {
    const artifact = await this.prisma.iiosAiArtifact.findUnique({
      where: { id },
      include: { claims: true, evidence: true, modelRun: true, job: true },
    });
    if (!artifact) throw new NotFoundException('artifact not found');
    await this.actors.assertOwns(principal, artifact.job.scopeId); // tenant fence (KG-02)
    return artifact;
  }

  /** Human acceptance feedback. Records who accepted; does NOT send or approve any route. */
  async accept(artifactId: string, principal: MessagePrincipal) {
    const artifact = await this.prisma.iiosAiArtifact.findUnique({ where: { id: artifactId }, include: { job: true } });
    if (!artifact) throw new NotFoundException('artifact not found');
    await this.actors.assertOwns(principal, artifact.job.scopeId); // tenant fence (KG-02)
    const actor = await this.actors.resolveActor(artifact.job.scopeId, principal);
    const updated = await this.prisma.iiosAiArtifact.update({
      where: { id: artifactId },
      data: { status: 'ACCEPTED', acceptedByActorId: actor.id },
    });
    await this.prisma.iiosAiClaim.updateMany({
      where: { artifactId },
      data: { validationStatus: 'ACCEPTED', acceptedByRef: actor.id },
    });
    await this.emit(IIOS_EVENTS.aiArtifactAccepted, artifact.job.scopeId, artifactId, { artifactId, acceptedByActorId: actor.id });
    await recordAudit(this.prisma, { action: 'ai.artifact.accepted', resourceType: 'ai_artifact', resourceId: artifactId, scopeId: artifact.job.scopeId, actorRefId: actor.id });
    return updated;
  }

  async reject(artifactId: string, principal: MessagePrincipal) {
    const artifact = await this.prisma.iiosAiArtifact.findUnique({ where: { id: artifactId }, include: { job: true } });
    if (!artifact) throw new NotFoundException('artifact not found');
    await this.actors.assertOwns(principal, artifact.job.scopeId); // tenant fence (KG-02)
    const actor = await this.actors.resolveActor(artifact.job.scopeId, principal);
    const updated = await this.prisma.iiosAiArtifact.update({
      where: { id: artifactId },
      data: { status: 'REJECTED', acceptedByActorId: actor.id },
    });
    await this.prisma.iiosAiClaim.updateMany({ where: { artifactId }, data: { validationStatus: 'REJECTED', acceptedByRef: actor.id } });
    await recordAudit(this.prisma, { action: 'ai.artifact.rejected', resourceType: 'ai_artifact', resourceId: artifactId, scopeId: artifact.job.scopeId, actorRefId: actor.id });
    return updated;
  }

  private async emit(type: string, scopeId: string, aggregateId: string, data: Record<string, unknown>): Promise<void> {
    const tid = currentTraceId() ?? newTraceId();
    const event: CloudEvent = {
      specversion: '1.0',
      id: `evt_ai_${aggregateId}_${type.split('.').slice(-2, -1)[0]}`,
      type,
      source: `iios/ai/${scopeId}`,
      subject: `ai_artifact/${aggregateId}`,
      time: new Date().toISOString(),
      datacontenttype: 'application/json',
      traceparent: toTraceparent(tid),
      insignia: { scopeSnapshotId: scopeId, correlationId: tid, idempotencyKey: `${type}:${aggregateId}`, dataClass: 'internal' },
      data,
    };
    await this.prisma.iiosOutboxEvent.create({
      data: {
        aggregateType: 'ai_artifact',
        aggregateId,
        eventType: type,
        cloudEvent: event as unknown as Prisma.InputJsonValue,
        partitionKey: `${scopeId}:${aggregateId}`,
      },
    });
  }
}
