import { ConflictException, Injectable } from '@nestjs/common';
import { createHash } from 'node:crypto';
import { PrismaService } from '../prisma/prisma.service';

const hash = (v: unknown) => createHash('sha256').update(stable(v)).digest('hex');
// Stable stringify: sort object keys so {a,b} and {b,a} hash equal.
const stable = (v: unknown): string =>
  v === null || typeof v !== 'object'
    ? JSON.stringify(v) ?? 'null'
    : Array.isArray(v)
      ? `[${v.map(stable).join(',')}]`
      : `{${Object.keys(v as object)
          .sort()
          .map((k) => `${JSON.stringify(k)}:${stable((v as Record<string, unknown>)[k])}`)
          .join(',')}}`;

export interface IdempotencyParams {
  scopeId?: string;
  commandName: string;
  key: string;
  request: unknown;
  actorRefId?: string;
}

/**
 * Idempotent-command ledger (P9, KG-06). Wraps an externally-visible mutation so a
 * retry with the same (scope, command, key) replays the cached response instead of
 * re-executing; the same key with a different request body is rejected as a conflict.
 * Write IN_FLIGHT at command start → COMPLETED (+ cached response) or FAILED on throw.
 */
@Injectable()
export class IdempotencyService {
  constructor(private readonly prisma: PrismaService) {}

  async run<T>(p: IdempotencyParams, fn: () => Promise<T>): Promise<T> {
    const requestHash = hash(p.request);
    const scopeId = p.scopeId ?? null;
    const where = { scopeId, commandName: p.commandName, idempotencyKey: p.key };

    const existing = await this.prisma.iiosIdempotencyCommand.findFirst({ where });
    if (existing) return this.resolveExisting<T>(where, existing, requestHash, fn);

    try {
      await this.prisma.iiosIdempotencyCommand.create({
        data: { scopeId, commandName: p.commandName, idempotencyKey: p.key, requestHash, actorRefId: p.actorRefId },
      });
    } catch {
      // Lost the create race: another caller inserted the same (scope, command, key).
      const row = await this.prisma.iiosIdempotencyCommand.findFirst({ where });
      if (row) return this.resolveExisting<T>(where, row, requestHash, fn);
      throw new ConflictException('idempotent command conflict');
    }

    return this.executeAndRecord<T>(where, fn);
  }

  private async resolveExisting<T>(
    where: { scopeId: string | null; commandName: string; idempotencyKey: string },
    row: { status: string; requestHash: string; response: unknown },
    requestHash: string,
    fn: () => Promise<T>,
  ): Promise<T> {
    if (row.requestHash !== requestHash) throw new ConflictException('idempotency key reused with a different request');
    if (row.status === 'COMPLETED') return row.response as T; // replay — no re-execution
    if (row.status === 'IN_FLIGHT') throw new ConflictException('command in progress');
    return this.executeAndRecord<T>(where, fn); // FAILED → retry + re-record the outcome
  }

  /** Run the command, then record COMPLETED (+ cached response) or FAILED against its ledger row. */
  private async executeAndRecord<T>(
    where: { scopeId: string | null; commandName: string; idempotencyKey: string },
    fn: () => Promise<T>,
  ): Promise<T> {
    try {
      const result = await fn();
      await this.prisma.iiosIdempotencyCommand.updateMany({
        where,
        data: { status: 'COMPLETED', responseHash: hash(result), response: result as object, completedAt: new Date() },
      });
      return result;
    } catch (err) {
      await this.prisma.iiosIdempotencyCommand.updateMany({ where, data: { status: 'FAILED', completedAt: new Date() } });
      throw err;
    }
  }
}
