import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
import { join } from "node:path";

const RETENTION_MS = 31 * 24 * 60 * 60 * 1000;
const SAMPLE_INTERVAL_MS = 5 * 60 * 1000;

export function createUsageHistoryStore(dataDir) {
  const filePath = dataDir ? join(dataDir, "usage-history.json") : "";
  const temporaryPath = dataDir ? join(dataDir, "usage-history.json.tmp") : "";
  let memoryState = { accounts: {} };
  let mutationQueue = Promise.resolve();

  async function load() {
    if (!filePath) return memoryState;
    try {
      const parsed = JSON.parse(await readFile(filePath, "utf8"));
      return normalizeState(parsed);
    } catch (error) {
      if (error.code === "ENOENT" || error instanceof SyntaxError) return { accounts: {} };
      throw error;
    }
  }

  async function save(state) {
    memoryState = state;
    if (!filePath) return;
    await mkdir(dataDir, { recursive: true });
    await writeFile(temporaryPath, JSON.stringify(state, null, 2), "utf8");
    await rename(temporaryPath, filePath);
  }

  return {
    async record(accountId, usage, now = new Date()) {
      validateAccountId(accountId);
      const point = usagePoint(usage, now);
      if (!point) return null;
      const operation = mutationQueue.then(async () => {
        const state = await load();
        const cutoff = now.getTime() - RETENTION_MS;
        const points = (state.accounts[accountId] ?? []).filter((item) => Date.parse(item.at) >= cutoff);
        const last = points.at(-1);
        if (last && !pointChanged(last, point) && Date.parse(point.at) - Date.parse(last.at) < SAMPLE_INTERVAL_MS) return last;
        points.push(point);
        state.accounts[accountId] = points;
        await save(state);
        return point;
      });
      mutationQueue = operation.catch(() => {});
      return await operation;
    },
    async history(accountId, since = new Date(0)) {
      validateAccountId(accountId);
      const threshold = since.getTime();
      const state = await load();
      return (state.accounts[accountId] ?? []).filter((point) => Date.parse(point.at) >= threshold);
    }
  };
}

function normalizeState(value) {
  const accounts = {};
  if (!value?.accounts || typeof value.accounts !== "object" || Array.isArray(value.accounts)) return { accounts };
  for (const [accountId, points] of Object.entries(value.accounts)) {
    if (!validAccountId(accountId) || !Array.isArray(points)) continue;
    accounts[accountId] = points.map(normalizePoint).filter(Boolean).sort((left, right) => left.at.localeCompare(right.at));
  }
  return { accounts };
}

function usagePoint(usage, now) {
  if (!Number.isFinite(now?.getTime?.())) return null;
  const point = {
    at: now.toISOString(),
    primaryRemainingPercent: safePercent(usage?.primary?.remainingPercent),
    primaryResetsAt: safeDate(usage?.primary?.resetsAt),
    secondaryRemainingPercent: safePercent(usage?.secondary?.remainingPercent),
    secondaryResetsAt: safeDate(usage?.secondary?.resetsAt)
  };
  return Number.isFinite(point.primaryRemainingPercent) || Number.isFinite(point.secondaryRemainingPercent) ? point : null;
}

function normalizePoint(value) {
  if (!value || !Number.isFinite(Date.parse(value.at))) return null;
  return usagePoint({
    primary: { remainingPercent: value.primaryRemainingPercent, resetsAt: value.primaryResetsAt },
    secondary: { remainingPercent: value.secondaryRemainingPercent, resetsAt: value.secondaryResetsAt }
  }, new Date(value.at));
}

function safePercent(value) {
  return Number.isFinite(value) ? Math.max(0, Math.min(100, value)) : null;
}

function safeDate(value) {
  return typeof value === "string" && Number.isFinite(Date.parse(value)) ? new Date(value).toISOString() : null;
}

function pointChanged(left, right) {
  return left.primaryRemainingPercent !== right.primaryRemainingPercent
    || left.primaryResetsAt !== right.primaryResetsAt
    || left.secondaryRemainingPercent !== right.secondaryRemainingPercent
    || left.secondaryResetsAt !== right.secondaryResetsAt;
}

function validateAccountId(value) {
  if (validAccountId(value)) return;
  const error = new Error("invalid_account_id");
  error.status = 400;
  throw error;
}

function validAccountId(value) {
  return typeof value === "string" && value.length > 0 && value.length <= 100 && !/[\\/\u0000-\u001f]/.test(value);
}
