const fs = require("fs");
const path = require("path");
const crypto = require("crypto");
const { getPrintDataDir, ensureDir } = require("./paths");

const MAX_QUEUE = Math.max(
  10,
  parseInt(process.env.PRINT_MAX_QUEUE || "500", 10) || 500,
);
const MAX_ATTEMPTS = Math.max(
  1,
  parseInt(process.env.PRINT_MAX_ATTEMPTS || "8", 10) || 8,
);
const MAX_JOBS_STORED = Math.max(
  100,
  parseInt(process.env.PRINT_MAX_JOBS_STORED || "5000", 10) || 5000,
);
const RETENTION_DAYS = Math.max(
  1,
  parseInt(process.env.PRINT_RETENTION_DAYS || "30", 10) || 30,
);
const API_ACK_URL = (process.env.SIRAYA_API_URL || "").trim().replace(/\/$/, "");
const API_ACK_TOKEN = (process.env.SIRAYA_PRINT_ACK_TOKEN || "").trim();

let store = { jobs: {} };
let writeChain = Promise.resolve();
let printPayloadToDevice = async () => {};
let processing = false;
let lastPumpAt = Date.now();
let lastPrintAt = null;

function markPumpActivity() {
  lastPumpAt = Date.now();
}

function getLastPumpAt() {
  return lastPumpAt;
}

const metrics = {
  walAppends: 0,
  checkpoints: 0,
  replayLinesApplied: 0,
  startupWalBytes: 0,
};

let STORE_PATH;
let WAL_PATH;
let WAL_META_PATH;

function initQueue({ printFn }) {
  printPayloadToDevice = printFn;
  const dataDir = getPrintDataDir();
  ensureDir(dataDir);
  STORE_PATH = path.join(dataDir, "print-store.json");
  WAL_PATH = path.join(dataDir, "print-journal.ndjson");
  WAL_META_PATH = path.join(dataDir, "print-wal-checkpoint.json");
  loadStore();
  verifyStartup();
  resumeQueuedJobs();
}

function readWalMeta() {
  try {
    if (!fs.existsSync(WAL_META_PATH)) return { checkpointWalBytes: 0 };
    const o = JSON.parse(fs.readFileSync(WAL_META_PATH, "utf8"));
    const n = Number(o.checkpointWalBytes);
    return { checkpointWalBytes: Number.isFinite(n) && n >= 0 ? n : 0 };
  } catch {
    return { checkpointWalBytes: 0 };
  }
}

function replayWalAfterCheckpoint() {
  if (!fs.existsSync(WAL_PATH)) return;
  const { checkpointWalBytes } = readWalMeta();
  const sz = fs.statSync(WAL_PATH).size;
  metrics.startupWalBytes = sz;
  if (checkpointWalBytes >= sz) return;
  const fd = fs.openSync(WAL_PATH, "r");
  try {
    const len = sz - checkpointWalBytes;
    const buf = Buffer.alloc(len);
    fs.readSync(fd, buf, 0, len, checkpointWalBytes);
    for (const line of buf.toString("utf8").split("\n")) {
      const t = line.trim();
      if (!t) continue;
      try {
        const o = JSON.parse(t);
        if (o && o.op === "put" && o.id && o.job) {
          store.jobs[o.id] = o.job;
          metrics.replayLinesApplied += 1;
        }
      } catch {
        console.warn("[print-wal] skipped bad journal line");
      }
    }
  } finally {
    fs.closeSync(fd);
  }
}

function loadStore() {
  try {
    if (fs.existsSync(STORE_PATH)) {
      const raw = fs.readFileSync(STORE_PATH, "utf8");
      store = JSON.parse(raw);
      if (!store || typeof store !== "object") store = { jobs: {} };
      if (!store.jobs) store.jobs = {};
    }
  } catch {
    store = { jobs: {} };
  }
  replayWalAfterCheckpoint();
}

function appendWalPut(jobId) {
  const job = store.jobs[jobId];
  if (!job) return;
  metrics.walAppends += 1;
  const line = `${JSON.stringify({ v: 1, op: "put", id: jobId, job })}\n`;
  fs.appendFile(WAL_PATH, line, "utf8", (err) => {
    if (err) console.warn("[print-wal] append failed", err.message);
  });
}

function persistStore() {
  writeChain = writeChain.then(
    () =>
      new Promise((resolve, reject) => {
        const tmp = `${STORE_PATH}.${process.pid}.tmp`;
        fs.writeFile(tmp, JSON.stringify(store), "utf8", (err) => {
          if (err) return reject(err);
          fs.rename(tmp, STORE_PATH, (e2) => {
            if (e2) return reject(e2);
            try {
              const walSz = fs.existsSync(WAL_PATH)
                ? fs.statSync(WAL_PATH).size
                : 0;
              fs.writeFileSync(
                WAL_META_PATH,
                JSON.stringify({ checkpointWalBytes: walSz }),
              );
              metrics.checkpoints += 1;
            } catch (e3) {
              console.warn("[print-wal] checkpoint meta write failed", e3);
            }
            resolve();
          });
        });
      }),
  );
  return writeChain;
}

function persistAfterJobMutation(jobId) {
  appendWalPut(jobId);
  return persistStore();
}

function queueDepth() {
  return Object.values(store.jobs).filter(
    (j) => j.state === "queued" || j.state === "processing",
  ).length;
}

async function maybePostServerAck(printJobId, orderId) {
  if (!API_ACK_URL || !API_ACK_TOKEN) return;
  try {
    const url = new URL("print-jobs/ack", `${API_ACK_URL}/`);
    const res = await fetch(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${API_ACK_TOKEN}`,
      },
      body: JSON.stringify({ printJobId, orderId, status: "printed" }),
    });
    if (!res.ok) {
      console.warn(`[ack] HTTP ${res.status} for printJobId=${printJobId}`);
    }
  } catch (e) {
    console.warn("[ack] failed", e);
  }
}

async function processJob(id) {
  const job = store.jobs[id];
  if (!job || job.state !== "queued") return;
  job.state = "processing";
  job.attempts = (job.attempts || 0) + 1;
  job.updatedAt = new Date().toISOString();
  await persistAfterJobMutation(id);

  try {
    await printPayloadToDevice(job);
    job.state = "printed";
    job.lastError = null;
    job.updatedAt = new Date().toISOString();
    lastPrintAt = job.updatedAt;
    await persistAfterJobMutation(id);
    await maybePostServerAck(job.printJobId || id, job.orderId);
  } catch (e) {
    const msg = e instanceof Error ? e.message : String(e);
    job.lastError = msg;
    if (job.attempts >= MAX_ATTEMPTS) {
      job.state = "dead";
    } else {
      job.state = "queued";
      const delay = Math.min(60_000, 500 * 2 ** Math.min(job.attempts, 8));
      setTimeout(() => void processJob(id), delay);
    }
    job.updatedAt = new Date().toISOString();
    await persistAfterJobMutation(id);
  }
}

function hashPayload(payload) {
  return crypto
    .createHash("sha256")
    .update(JSON.stringify(payload || {}))
    .digest("hex");
}

function findByPayloadHash(hash) {
  if (!hash) return null;
  for (const j of Object.values(store.jobs)) {
    if (j.payloadHash === hash && j.state === "printed") return j;
  }
  return null;
}

function pruneOldJobs() {
  const cutoff = Date.now() - RETENTION_DAYS * 86400000;
  const ids = Object.keys(store.jobs);
  if (ids.length <= MAX_JOBS_STORED) return;
  const sorted = ids
    .map((id) => ({ id, job: store.jobs[id] }))
    .filter(({ job }) => job.state === "printed" || job.state === "dead")
    .sort(
      (a, b) =>
        new Date(a.job.updatedAt || 0).getTime() -
        new Date(b.job.updatedAt || 0).getTime(),
    );
  let removed = 0;
  while (Object.keys(store.jobs).length > MAX_JOBS_STORED && sorted.length) {
    const { id, job } = sorted.shift();
    const ts = new Date(job.updatedAt || 0).getTime();
    if (ts < cutoff || Object.keys(store.jobs).length > MAX_JOBS_STORED) {
      delete store.jobs[id];
      removed += 1;
    }
  }
  if (removed > 0) void persistStore();
}

function pumpQueue() {
  if (processing) return;
  processing = true;
  markPumpActivity();
  const run = async () => {
    try {
      const pending = Object.keys(store.jobs).filter(
        (id) => store.jobs[id].state === "queued",
      );
      for (const id of pending) {
        markPumpActivity();
        await processJob(id);
      }
      pruneOldJobs();
    } finally {
      processing = false;
      markPumpActivity();
      if (
        Object.values(store.jobs).some((j) => j.state === "queued")
      ) {
        setImmediate(pumpQueue);
      }
    }
  };
  void run();
}

function resumeQueuedJobs() {
  const queued = Object.keys(store.jobs).filter(
    (id) =>
      store.jobs[id].state === "queued" ||
      store.jobs[id].state === "processing",
  );
  for (const id of queued) {
    if (store.jobs[id].state === "processing") {
      store.jobs[id].state = "queued";
    }
  }
  if (queued.length > 0) pumpQueue();
}

function acceptPrintJob(body, { legacy = false } = {}) {
  if (queueDepth() >= MAX_QUEUE) {
    const err = new Error("QUEUE_FULL");
    err.status = 503;
    throw err;
  }

  if (legacy) {
    const printJobId = `legacy-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
    const job = {
      printJobId,
      idempotencyKey: printJobId,
      receiptKind: "customer_receipt",
      templateVersion: 1,
      schemaVersion: 1,
      payload: body,
      state: "queued",
      attempts: 0,
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString(),
    };
    store.jobs[printJobId] = job;
    void persistAfterJobMutation(printJobId).then(() => pumpQueue());
    return { printJobId, state: job.state };
  }

  const printJobId = String(body.printJobId || "").trim();
  const idem = String(body.idempotencyKey || printJobId).trim();
  if (!printJobId || printJobId.length < 4) {
    const err = new Error("INVALID_PRINT_JOB_ID");
    err.status = 400;
    throw err;
  }

  const payloadHash =
    String(body.payloadHash || "").trim() ||
    hashPayload(body.payload);

  const existing = store.jobs[printJobId];
  if (existing && existing.idempotencyKey !== idem) {
    const err = new Error("PRINT_JOB_ID_CONFLICT");
    err.status = 409;
    throw err;
  }
  if (existing && existing.idempotencyKey === idem) {
    if (existing.state === "queued" || existing.state === "processing") {
      pumpQueue();
    }
    return { printJobId, state: existing.state, deduped: true };
  }

  const hashDup = findByPayloadHash(payloadHash);
  if (hashDup && hashDup.idempotencyKey === idem) {
    return {
      printJobId: hashDup.printJobId,
      state: hashDup.state,
      deduped: true,
    };
  }

  const job = {
    ...body,
    printJobId,
    idempotencyKey: idem,
    payloadHash,
    state: "queued",
    attempts: 0,
    createdAt: new Date().toISOString(),
    updatedAt: new Date().toISOString(),
  };
  store.jobs[printJobId] = job;
  void persistAfterJobMutation(printJobId).then(() => pumpQueue());
  return { printJobId, state: "queued" };
}

function getJob(id) {
  return store.jobs[id] || null;
}

function replayJob(id, { force = false } = {}) {
  const j = store.jobs[id];
  if (!j) return null;
  if (!force && (j.state === "printed" || j.state === "dead")) {
    const err = new Error("USE_force_QUERY_TO_REPRINT_TERMINAL_STATE");
    err.status = 409;
    throw err;
  }
  j.state = "queued";
  j.attempts = 0;
  j.updatedAt = new Date().toISOString();
  void persistAfterJobMutation(id).then(() => pumpQueue());
  return { printJobId: id, state: "queued" };
}

function verifyStartup() {
  console.log(
    `[print-startup] jobs=${Object.keys(store.jobs).length} queueDepth=${queueDepth()} walReplayLines=${metrics.replayLinesApplied} walBytes=${metrics.startupWalBytes} dataDir=${getPrintDataDir()}`,
  );
}

function metricsText() {
  const q = queueDepth();
  const lines = [
    "# HELP siraya_print_agent_queue_depth Print jobs queued or processing.",
    "# TYPE siraya_print_agent_queue_depth gauge",
    `siraya_print_agent_queue_depth ${q}`,
    "# HELP siraya_print_agent_jobs_total Jobs known in store.",
    "# TYPE siraya_print_agent_jobs_total gauge",
    `siraya_print_agent_jobs_total ${Object.keys(store.jobs).length}`,
    "# HELP siraya_print_agent_wal_appends_total Append-only journal lines written.",
    "# TYPE siraya_print_agent_wal_appends_total counter",
    `siraya_print_agent_wal_appends_total ${metrics.walAppends}`,
    "# HELP siraya_print_agent_checkpoints_total JSON checkpoint writes completed.",
    "# TYPE siraya_print_agent_checkpoints_total counter",
    `siraya_print_agent_checkpoints_total ${metrics.checkpoints}`,
  ];
  return `${lines.join("\n")}\n`;
}

function getHealthExtras() {
  const walSz = fs.existsSync(WAL_PATH) ? fs.statSync(WAL_PATH).size : 0;
  return {
    queueDepth: queueDepth(),
    storePath: STORE_PATH,
    walPath: WAL_PATH,
    walBytes: walSz,
    checkpointWalBytes: readWalMeta().checkpointWalBytes,
    lastPrintAt,
    jobsTotal: Object.keys(store.jobs).length,
  };
}

function flushQueue() {
  return writeChain;
}

function listJobs({ limit = 50, offset = 0 } = {}) {
  const all = Object.values(store.jobs).sort(
    (a, b) =>
      new Date(b.updatedAt || b.createdAt || 0).getTime() -
      new Date(a.updatedAt || a.createdAt || 0).getTime(),
  );
  const slice = all.slice(offset, offset + limit);
  return {
    total: all.length,
    limit,
    offset,
    jobs: slice.map((j) => ({
      printJobId: j.printJobId,
      receiptKind: j.receiptKind,
      orderId: j.orderId,
      state: j.state,
      attempts: j.attempts,
      lastError: j.lastError || null,
      createdAt: j.createdAt,
      updatedAt: j.updatedAt,
    })),
  };
}

module.exports = {
  initQueue,
  acceptPrintJob,
  getJob,
  replayJob,
  listJobs,
  queueDepth,
  metricsText,
  getHealthExtras,
  getPrintDataDir,
  flushQueue,
  hashPayload,
  markPumpActivity,
  getLastPumpAt,
};
