import { createHash, randomUUID } from "node:crypto";
import { mkdir, open, readFile, rename, rm } from "node:fs/promises";
import { join } from "node:path";

const SCHEMA_VERSION = 1;
const DEFAULT_LIFETIME_MS = 7 * 24 * 60 * 60 * 1000;
const DEFAULT_MAX_RECORDS = 4096;
const MESSAGE_STATES = new Set(["accepted", "dispatching", "dispatched", "steering", "steered", "deleted", "indeterminate"]);
const APPROVAL_DECISION_STATES = new Set(["dispatching", "resolved", "indeterminate"]);
const QUEUED_ACTION_STATES = new Set(["dispatching", "completed", "indeterminate"]);

export function createActionReceiptStore(dataDir, dependencies = {}) {
  const now = dependencies.now ?? (() => new Date());
  const lifetimeMs = positiveInteger(dependencies.lifetimeMs, DEFAULT_LIFETIME_MS);
  const maxRecords = positiveInteger(dependencies.maxRecords, DEFAULT_MAX_RECORDS);
  const filePath = dataDir ? join(dataDir, "action-receipts.json") : "";
  let records = [];
  let initialized = false;
  let unavailable = false;
  let mutationQueue = Promise.resolve();

  async function initialize() {
    if (initialized) return;
    initialized = true;
    if (!filePath) return;
    try {
      const loaded = JSON.parse(await readFile(filePath, "utf8"));
      records = normalizeState(loaded);
      if (records.length > maxRecords) throw new SyntaxError("action receipt capacity exceeded");
      const retained = unexpiredRecords(records, now());
      if (retained.length !== records.length) {
        records = retained;
        await writeState(dataDir, filePath, records);
      }
    } catch (error) {
      if (error.code === "ENOENT") return;
      unavailable = true;
    }
  }

  async function findMessage({ sessionId, clientMessageId, payloadHash }) {
    assertAvailable();
    const receipt = activeMessageReceipt(records, messageKeyHash(sessionId, clientMessageId), now());
    if (!receipt) return null;
    assertMatchingPayload(receipt, payloadHash);
    return cloneReceipt(receipt);
  }

  async function getMessage({ sessionId, clientMessageId }) {
    assertAvailable();
    const receipt = activeMessageReceipt(records, messageKeyHash(sessionId, clientMessageId), now());
    return receipt ? cloneReceipt(receipt) : null;
  }

  async function reserveMessage({ sessionId, clientMessageId, payloadHash, delivery, state = "accepted" }) {
    return mutate(async () => {
      records = unexpiredRecords(records, now());
      const keyHash = messageKeyHash(sessionId, clientMessageId);
      const existing = records.find((item) => item.kind === "message" && item.keyHash === keyHash);
      if (existing) {
        assertMatchingPayload(existing, payloadHash);
        return { created: false, receipt: cloneReceipt(existing) };
      }
      if (records.length >= maxRecords) throw receiptError("idempotency_capacity_exceeded", 503);
      if (!MESSAGE_STATES.has(state)) throw receiptError("idempotency_store_unavailable", 503);
      const createdAt = safeNow(now());
      const receipt = {
        kind: "message",
        keyHash,
        payloadHash,
        delivery: normalizeDelivery(delivery),
        state,
        createdAt,
        expiresAt: new Date(new Date(createdAt).getTime() + lifetimeMs).toISOString()
      };
      records.push(receipt);
      await persist();
      return { created: true, receipt: cloneReceipt(receipt) };
    });
  }

  async function updateMessage({ sessionId, clientMessageId, state }) {
    return mutate(async () => {
      if (!MESSAGE_STATES.has(state)) throw receiptError("idempotency_store_unavailable", 503);
      const receipt = records.find((item) => item.kind === "message" && item.keyHash === messageKeyHash(sessionId, clientMessageId));
      if (!receipt) throw receiptError("idempotency_store_unavailable", 503);
      receipt.state = state;
      await persist();
      return cloneReceipt(receipt);
    });
  }

  async function reserveApprovalNotification(input) {
    return mutate(async () => {
      records = unexpiredRecords(records, now());
      const keyHash = approvalNotificationKeyHash(input);
      const existing = records.find((item) => item.kind === "approval_notification" && item.keyHash === keyHash);
      if (existing) return { created: false, receipt: cloneReceipt(existing) };
      if (records.length >= maxRecords) throw receiptError("idempotency_capacity_exceeded", 503);
      const approvalId = boundedId(input.approvalId);
      const createdAt = safeNow(now());
      const receipt = {
        kind: "approval_notification",
        keyHash,
        approvalId,
        createdAt,
        expiresAt: new Date(new Date(createdAt).getTime() + lifetimeMs).toISOString()
      };
      records.push(receipt);
      await persist();
      return { created: true, receipt: cloneReceipt(receipt) };
    });
  }

  async function findApprovalDecision({ sessionId, approvalId, decision }) {
    assertAvailable();
    const receipt = activeReceipt(records, "approval_decision", approvalDecisionKeyHash(sessionId, approvalId), now());
    if (!receipt) return null;
    assertMatchingPayload(receipt, approvalDecisionPayloadHash(decision));
    return cloneReceipt(receipt);
  }

  async function reserveApprovalDecision({ sessionId, approvalId, decision, delivery, state = "dispatching" }) {
    return mutate(async () => {
      records = unexpiredRecords(records, now());
      const keyHash = approvalDecisionKeyHash(sessionId, approvalId);
      const payloadHash = approvalDecisionPayloadHash(decision);
      const existing = records.find((item) => item.kind === "approval_decision" && item.keyHash === keyHash);
      if (existing) {
        assertMatchingPayload(existing, payloadHash);
        return { created: false, receipt: cloneReceipt(existing) };
      }
      if (records.length >= maxRecords) throw receiptError("idempotency_capacity_exceeded", 503);
      if (!APPROVAL_DECISION_STATES.has(state)) throw receiptError("idempotency_store_unavailable", 503);
      const createdAt = safeNow(now());
      const receipt = {
        kind: "approval_decision",
        keyHash,
        payloadHash,
        delivery: normalizeApprovalDelivery(delivery),
        state,
        createdAt,
        expiresAt: new Date(new Date(createdAt).getTime() + lifetimeMs).toISOString()
      };
      records.push(receipt);
      await persist();
      return { created: true, receipt: cloneReceipt(receipt) };
    });
  }

  async function updateApprovalDecision({ sessionId, approvalId, state }) {
    return mutate(async () => {
      if (!APPROVAL_DECISION_STATES.has(state)) throw receiptError("idempotency_store_unavailable", 503);
      const receipt = records.find((item) =>
        item.kind === "approval_decision" && item.keyHash === approvalDecisionKeyHash(sessionId, approvalId));
      if (!receipt) throw receiptError("idempotency_store_unavailable", 503);
      receipt.state = state;
      await persist();
      return cloneReceipt(receipt);
    });
  }

  async function findQueuedAction({ sessionId, queuedMessageId, action }) {
    assertAvailable();
    const receipt = activeReceipt(records, "queued_action", queuedActionKeyHash(sessionId, queuedMessageId), now());
    if (!receipt) return null;
    assertMatchingPayload(receipt, queuedActionPayloadHash(action));
    return cloneReceipt(receipt);
  }

  async function reserveQueuedAction({ sessionId, queuedMessageId, action, delivery, state = "dispatching" }) {
    return mutate(async () => {
      records = unexpiredRecords(records, now());
      const keyHash = queuedActionKeyHash(sessionId, queuedMessageId);
      const payloadHash = queuedActionPayloadHash(action);
      const existing = records.find((item) => item.kind === "queued_action" && item.keyHash === keyHash);
      if (existing) {
        assertMatchingPayload(existing, payloadHash);
        return { created: false, receipt: cloneReceipt(existing) };
      }
      if (records.length >= maxRecords) throw receiptError("idempotency_capacity_exceeded", 503);
      if (!QUEUED_ACTION_STATES.has(state)) throw receiptError("idempotency_store_unavailable", 503);
      const createdAt = safeNow(now());
      const receipt = {
        kind: "queued_action",
        keyHash,
        payloadHash,
        delivery: normalizeQueuedActionDelivery(delivery),
        state,
        createdAt,
        expiresAt: new Date(new Date(createdAt).getTime() + lifetimeMs).toISOString()
      };
      records.push(receipt);
      await persist();
      return { created: true, receipt: cloneReceipt(receipt) };
    });
  }

  async function updateQueuedAction({ sessionId, queuedMessageId, state }) {
    return mutate(async () => {
      if (!QUEUED_ACTION_STATES.has(state)) throw receiptError("idempotency_store_unavailable", 503);
      const receipt = records.find((item) =>
        item.kind === "queued_action" && item.keyHash === queuedActionKeyHash(sessionId, queuedMessageId));
      if (!receipt) throw receiptError("idempotency_store_unavailable", 503);
      receipt.state = state;
      await persist();
      return cloneReceipt(receipt);
    });
  }

  function assertAvailable() {
    if (!initialized || unavailable) throw receiptError("idempotency_store_unavailable", 503);
  }

  function mutate(operation) {
    const pending = mutationQueue.then(async () => {
      assertAvailable();
      return await operation();
    });
    mutationQueue = pending.catch(() => {});
    return pending;
  }

  async function persist() {
    if (!filePath) return;
    try {
      await writeState(dataDir, filePath, records);
    } catch {
      unavailable = true;
      throw receiptError("idempotency_store_unavailable", 503);
    }
  }

  return {
    initialize,
    findMessage,
    getMessage,
    reserveMessage,
    updateMessage,
    reserveApprovalNotification,
    findApprovalDecision,
    reserveApprovalDecision,
    updateApprovalDecision,
    findQueuedAction,
    reserveQueuedAction,
    updateQueuedAction
  };
}

async function writeState(directory, filePath, records) {
  await mkdir(directory, { recursive: true });
  const temporaryPath = join(directory, `action-receipts.${process.pid}.${randomUUID()}.pending`);
  let handle;
  try {
    handle = await open(temporaryPath, "wx");
    await handle.writeFile(JSON.stringify({ schemaVersion: SCHEMA_VERSION, records }, null, 2));
    await handle.sync();
    await handle.close();
    handle = null;
    await rename(temporaryPath, filePath);
  } finally {
    await handle?.close().catch(() => {});
    await rm(temporaryPath, { force: true }).catch(() => {});
  }
}

function normalizeState(value) {
  if (value?.schemaVersion !== SCHEMA_VERSION || !Array.isArray(value.records)) throw new SyntaxError("invalid action receipt state");
  const seen = new Set();
  return value.records.map((record) => {
    if (!hexHash(record?.keyHash) || !validTimestamp(record.createdAt) || !validTimestamp(record.expiresAt)) {
      throw new SyntaxError("invalid action receipt record");
    }
    const duplicateKey = `${record.kind}\u0000${record.keyHash}`;
    if (seen.has(duplicateKey)) throw new SyntaxError("duplicate action receipt record");
    seen.add(duplicateKey);
    const timestamps = {
      createdAt: new Date(record.createdAt).toISOString(),
      expiresAt: new Date(record.expiresAt).toISOString()
    };
    if (record.kind === "message" && hexHash(record.payloadHash) && MESSAGE_STATES.has(record.state)) {
      return {
        kind: "message",
        keyHash: record.keyHash,
        payloadHash: record.payloadHash,
        delivery: normalizeDelivery(record.delivery),
        state: record.state,
        ...timestamps
      };
    }
    if (record.kind === "approval_notification") {
      return {
        kind: "approval_notification",
        keyHash: record.keyHash,
        approvalId: boundedId(record.approvalId),
        ...timestamps
      };
    }
    if (record.kind === "approval_decision"
      && hexHash(record.payloadHash)
      && APPROVAL_DECISION_STATES.has(record.state)) {
      return {
        kind: "approval_decision",
        keyHash: record.keyHash,
        payloadHash: record.payloadHash,
        delivery: normalizeApprovalDelivery(record.delivery),
        state: record.state,
        ...timestamps
      };
    }
    if (record.kind === "queued_action"
      && hexHash(record.payloadHash)
      && QUEUED_ACTION_STATES.has(record.state)) {
      return {
        kind: "queued_action",
        keyHash: record.keyHash,
        payloadHash: record.payloadHash,
        delivery: normalizeQueuedActionDelivery(record.delivery),
        state: record.state,
        ...timestamps
      };
    }
    throw new SyntaxError("invalid action receipt record");
  });
}

function normalizeDelivery(value) {
  if (value?.disposition === "started") return { disposition: "started" };
  if (value?.disposition === "queued" && typeof value.queuedMessageId === "string" && value.queuedMessageId.length <= 100) {
    return { disposition: "queued", queuedMessageId: value.queuedMessageId };
  }
  throw receiptError("idempotency_store_unavailable", 503);
}

function activeMessageReceipt(records, keyHash, currentTime) {
  return activeReceipt(records, "message", keyHash, currentTime);
}

function activeReceipt(records, kind, keyHash, currentTime) {
  const timestamp = safeNow(currentTime);
  return records.find((item) => item.kind === kind && item.keyHash === keyHash && item.expiresAt > timestamp);
}

function unexpiredRecords(records, currentTime) {
  const timestamp = safeNow(currentTime);
  return records.filter((item) => item.expiresAt > timestamp);
}

function assertMatchingPayload(receipt, payloadHash) {
  if (!hexHash(payloadHash) || receipt.payloadHash !== payloadHash) throw receiptError("idempotency_conflict", 409);
}

function messageKeyHash(sessionId, clientMessageId) {
  return createHash("sha256").update(`message\u0000${sessionId}\u0000${clientMessageId}`).digest("hex");
}

function approvalNotificationKeyHash({ sessionId, accountId, threadId, turnId, itemId, runtimeRequestId }) {
  if (runtimeRequestId == null) throw receiptError("idempotency_store_unavailable", 503);
  return createHash("sha256").update([
    "approval_notification",
    sessionId ?? "",
    accountId ?? "",
    threadId ?? "",
    turnId ?? "",
    itemId ?? "",
    String(runtimeRequestId)
  ].join("\u0000")).digest("hex");
}

function approvalDecisionKeyHash(sessionId, approvalId) {
  return createHash("sha256").update(`approval_decision\u0000${sessionId}\u0000${approvalId}`).digest("hex");
}

function approvalDecisionPayloadHash(decision) {
  if (!["approved", "approved_for_session", "approved_always", "rejected"].includes(decision)) {
    throw receiptError("invalid_approval_decision", 400);
  }
  return createHash("sha256").update(`approval_decision_payload\u0000${decision}`).digest("hex");
}

function normalizeApprovalDelivery(value) {
  const approvalId = boundedId(value?.approvalId);
  if (!["approved", "rejected"].includes(value?.status)) throw receiptError("idempotency_store_unavailable", 503);
  return { approvalId, status: value.status };
}

function queuedActionKeyHash(sessionId, queuedMessageId) {
  return createHash("sha256").update(`queued_action\u0000${sessionId}\u0000${queuedMessageId}`).digest("hex");
}

function queuedActionPayloadHash(action) {
  if (!["steer", "delete"].includes(action)) throw receiptError("idempotency_store_unavailable", 503);
  return createHash("sha256").update(`queued_action_payload\u0000${action}`).digest("hex");
}

function normalizeQueuedActionDelivery(value) {
  const queuedMessageId = boundedId(value?.queuedMessageId);
  if (!["steer", "delete"].includes(value?.action)) throw receiptError("idempotency_store_unavailable", 503);
  return { queuedMessageId, action: value.action };
}

function boundedId(value) {
  if (typeof value !== "string" || value.length === 0 || value.length > 100) {
    throw receiptError("idempotency_store_unavailable", 503);
  }
  return value;
}

function cloneReceipt(receipt) {
  return { ...receipt, ...(receipt.delivery ? { delivery: { ...receipt.delivery } } : {}) };
}

function receiptError(code, status) {
  const error = new Error(code);
  error.status = status;
  error.safeLogCode = code;
  return error;
}

function positiveInteger(value, fallback) {
  return Number.isSafeInteger(value) && value > 0 ? value : fallback;
}

function safeNow(value) {
  return Number.isFinite(value?.getTime?.()) ? value.toISOString() : new Date().toISOString();
}

function validTimestamp(value) {
  return typeof value === "string" && Number.isFinite(Date.parse(value));
}

function hexHash(value) {
  return typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
}
