import { randomUUID } from "node:crypto";
import { requirePermission } from "./auth.mjs";
import { exactCandidate, requestFingerprint } from "./candidate.mjs";
import { badRequest, conflict, LabError } from "./errors.mjs";

function elapsedMs(startedAt) {
  return Number((Number(process.hrtime.bigint() - startedAt) / 1_000_000).toFixed(3));
}

function validateText(value, name) {
  if (typeof value !== "string" || value.trim() === "") {
    throw badRequest("invalid_request", `${name} must be a non-empty string.`);
  }
  return value.trim();
}

function effectStateInDoubt(operationId) {
  return new LabError(
    503,
    "effect_state_in_doubt",
    "The recommendation may have committed, but source-of-record readback did not confirm it. Reconcile the operation before issuing another write.",
    { operationId },
  );
}

function buildRecommendation(claimCase) {
  if (
    claimCase.payer === "NorthLake" &&
    claimCase.originalClaim.modifier25Present &&
    claimCase.encounterEvidence.separatelyDocumentedSameDayEvaluation
  ) {
    return {
      disposition: "specialist_review_required",
      proposedCorrection: "Retain modifier 25 on the corrected claim.",
      rationale:
        "The NorthLake case contains a separately documented same-day evaluation and the original claim contains modifier 25.",
      safeFallback: "Do not submit automatically; route to an authenticated billing specialist.",
    };
  }

  return {
    disposition: "insufficient_evidence",
    proposedCorrection: null,
    rationale: "The bounded teaching rule does not support a correction for this case.",
    safeFallback: "Collect missing evidence and keep the case in specialist review.",
  };
}

export class ClaimsDenialService {
  #system;
  #telemetry;
  #idempotency = new Map();

  constructor({ system, telemetry }) {
    this.#system = system;
    this.#telemetry = telemetry;
  }

  getCase({ principal, caseId }) {
    requirePermission(principal, "case:read");
    return this.#system.getCase(principal.tenantId, validateText(caseId, "caseId"));
  }

  getOperation({ principal, operationId }) {
    requirePermission(principal, "operation:read");
    return this.#system.getOperation(
      principal.tenantId,
      validateText(operationId, "operationId"),
    );
  }

  getTelemetry({ principal }) {
    requirePermission(principal, "telemetry:read");
    return this.#telemetry.list(principal.tenantId);
  }

  async recommend({
    principal,
    caseId,
    expectedCaseRevision,
    candidateId,
    candidateDigest,
    idempotencyKey,
    failureMode,
  }) {
    requirePermission(principal, "recommendation:write");
    const startedAt = process.hrtime.bigint();
    const traceId = randomUUID();
    const normalizedCaseId = validateText(caseId, "caseId");
    const normalizedKey = validateText(idempotencyKey, "Idempotency-Key");

    if (!Number.isSafeInteger(expectedCaseRevision) || expectedCaseRevision < 0) {
      throw badRequest(
        "invalid_request",
        "expectedCaseRevision must be a non-negative integer.",
      );
    }
    if (candidateId !== exactCandidate.candidateId || candidateDigest !== exactCandidate.digest) {
      throw conflict(
        "candidate_mismatch",
        "The request is not bound to the exact candidate exposed by /v1/candidate.",
        {
          requiredCandidateId: exactCandidate.candidateId,
          requiredCandidateDigest: exactCandidate.digest,
        },
      );
    }
    if (failureMode && failureMode !== "after-write-before-response") {
      throw badRequest("invalid_failure_mode", "The requested failure mode is not supported.");
    }
    if (failureMode) {
      requirePermission(principal, "failure:inject");
    }

    const fingerprint = requestFingerprint({
      tenantId: principal.tenantId,
      caseId: normalizedCaseId,
      expectedCaseRevision,
      candidateId,
      candidateDigest,
    });
    const idempotencyScope = `${principal.tenantId}:${normalizedKey}`;
    const existing = this.#idempotency.get(idempotencyScope);
    if (existing && existing.fingerprint !== fingerprint) {
      throw conflict(
        "idempotency_key_reused",
        "The idempotency key was already used for a different request.",
      );
    }
    if (existing?.state === "committed") {
      this.#telemetry.record({
        traceId,
        operation: "recommendation",
        outcome: "idempotent_replay",
        tenantId: principal.tenantId,
        caseId: normalizedCaseId,
        candidateDigest,
        latencyMs: elapsedMs(startedAt),
        estimatedCostCad: 0,
      });
      return { ...structuredClone(existing.response), idempotentReplay: true };
    }
    if (existing?.state === "pending") {
      const response = await existing.promise;
      this.#telemetry.record({
        traceId,
        operation: "recommendation",
        outcome: "idempotent_replay",
        tenantId: principal.tenantId,
        caseId: normalizedCaseId,
        candidateDigest,
        latencyMs: elapsedMs(startedAt),
        estimatedCostCad: 0,
      });
      return { ...structuredClone(response), idempotentReplay: true };
    }
    if (existing?.state === "in_doubt") {
      this.#telemetry.record({
        traceId,
        operation: "recommendation",
        outcome: "replay_blocked_in_doubt",
        tenantId: principal.tenantId,
        caseId: normalizedCaseId,
        candidateDigest,
        latencyMs: elapsedMs(startedAt),
        estimatedCostCad: 0,
        errorCode: "effect_state_in_doubt",
      });
      throw effectStateInDoubt(existing.operationId);
    }

    const work = this.#performRecommendation({
      principal,
      caseId: normalizedCaseId,
      expectedCaseRevision,
      candidateId,
      candidateDigest,
      traceId,
      startedAt,
    });
    this.#idempotency.set(idempotencyScope, { state: "pending", fingerprint, promise: work });

    try {
      const response = await work;
      this.#idempotency.set(idempotencyScope, {
        state: "committed",
        fingerprint,
        response: structuredClone(response),
      });
      if (failureMode === "after-write-before-response") {
        this.#telemetry.record({
          operation: "recommendation_response",
          outcome: "uncertain_to_caller",
          tenantId: principal.tenantId,
          caseId: normalizedCaseId,
          candidateDigest,
          latencyMs: elapsedMs(startedAt),
          estimatedCostCad: 0,
          errorCode: "response_lost_after_commit",
        });
        throw new LabError(
          503,
          "response_lost_after_commit",
          "The write committed, but the response was lost. Read back the operation or retry with the same idempotency key.",
          { operationId: response.operationId },
        );
      }
      return response;
    } catch (error) {
      if (error instanceof LabError && error.code === "effect_state_in_doubt") {
        this.#idempotency.set(idempotencyScope, {
          state: "in_doubt",
          fingerprint,
          operationId: error.details.operationId,
        });
      } else if (!(error instanceof LabError && error.code === "response_lost_after_commit")) {
        this.#idempotency.delete(idempotencyScope);
      }
      throw error;
    }
  }

  async #performRecommendation({
    principal,
    caseId,
    expectedCaseRevision,
    candidateId,
    candidateDigest,
    traceId,
    startedAt,
  }) {
    await Promise.resolve();
    try {
      const claimCase = this.#system.getCase(principal.tenantId, caseId);
      const recommendation = {
        ...buildRecommendation(claimCase),
        candidateId,
        candidateDigest,
        generatedBy: "bounded-teaching-rule",
      };
      const operation = this.#system.writeRecommendation({
        tenantId: principal.tenantId,
        caseId,
        expectedRevision: expectedCaseRevision,
        recommendation,
      });
      let readback;
      try {
        readback = this.#system.getOperation(principal.tenantId, operation.operationId);
      } catch {
        throw effectStateInDoubt(operation.operationId);
      }
      if (
        readback.candidateId !== candidateId ||
        readback.candidateDigest !== candidateDigest ||
        readback.resultingCaseRevision !== expectedCaseRevision + 1
      ) {
        throw effectStateInDoubt(operation.operationId);
      }

      const response = {
        operationId: operation.operationId,
        caseId,
        resultingCaseRevision: operation.resultingCaseRevision,
        candidateId,
        candidateDigest,
        recommendation,
        authorityBoundary:
          "Recommendation recorded only; an authenticated billing specialist retains approval and submission authority.",
        idempotentReplay: false,
      };
      this.#telemetry.record({
        traceId,
        operation: "recommendation",
        outcome: "committed_and_read_back",
        tenantId: principal.tenantId,
        caseId,
        candidateDigest,
        latencyMs: elapsedMs(startedAt),
        estimatedCostCad: exactCandidate.estimatedCostCadPerRun,
      });
      return response;
    } catch (error) {
      this.#telemetry.record({
        traceId,
        operation: "recommendation",
        outcome:
          error instanceof LabError && error.code === "effect_state_in_doubt"
            ? "effect_state_in_doubt"
            : "rejected",
        tenantId: principal.tenantId,
        caseId,
        candidateDigest,
        latencyMs: elapsedMs(startedAt),
        estimatedCostCad: 0,
        errorCode: error.code ?? "internal_error",
      });
      throw error;
    }
  }
}
