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

const SUMMARY_FIELDS = [
  "lifetimeTokens",
  "peakDailyTokens",
  "currentStreakDays",
  "longestStreakDays",
  "longestRunningTurnSec"
];

export function createAccountTokenUsageStore(dataDir) {
  const filePath = dataDir ? join(dataDir, "account-token-usage.json") : "";
  const temporaryPath = dataDir ? join(dataDir, "account-token-usage.json.tmp") : "";
  let memoryState = emptyState();
  let mutationQueue = Promise.resolve();

  async function load() {
    if (!filePath) return memoryState;
    try {
      return normalizeState(JSON.parse(await readFile(filePath, "utf8")));
    } catch (error) {
      if (error.code === "ENOENT" || error instanceof SyntaxError) return emptyState();
      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);
  }

  async function mutate(operation) {
    const pending = mutationQueue.then(async () => {
      const state = await load();
      const result = operation(state);
      await save(state);
      return result;
    });
    mutationQueue = pending.catch(() => {});
    return await pending;
  }

  return {
    async record(accountId, usage, now = new Date()) {
      validateAccountId(accountId);
      const normalized = normalizeUsage(usage);
      return await mutate((state) => {
        const previous = state.accounts[accountId]?.usage;
        const merged = mergeUsage(previous, normalized);
        state.accounts[accountId] = { usage: merged, updatedAt: safeNow(now) };
        return merged;
      });
    },

    async read(accountId) {
      validateAccountId(accountId);
      const state = await load();
      return state.accounts[accountId]?.usage ?? null;
    },

    async detach(accountId, identity) {
      validateAccountId(accountId);
      const normalizedIdentity = normalizeIdentity(identity);
      if (!normalizedIdentity) return false;
      return await mutate((state) => {
        const snapshot = state.accounts[accountId];
        if (!snapshot) return false;
        const fingerprint = identityFingerprint(state, normalizedIdentity);
        state.detached[fingerprint] = snapshot;
        delete state.accounts[accountId];
        return true;
      });
    },

    async attach(accountId, identity) {
      validateAccountId(accountId);
      const normalizedIdentity = normalizeIdentity(identity);
      if (!normalizedIdentity) return null;
      return await mutate((state) => {
        const fingerprint = identityFingerprint(state, normalizedIdentity);
        const snapshot = state.detached[fingerprint];
        if (!snapshot) return null;
        state.accounts[accountId] = {
          usage: mergeUsage(state.accounts[accountId]?.usage, snapshot.usage),
          updatedAt: snapshot.updatedAt
        };
        delete state.detached[fingerprint];
        return state.accounts[accountId].usage;
      });
    }
  };
}

function emptyState() {
  return { version: 1, identitySalt: "", accounts: {}, detached: {} };
}

function normalizeState(value) {
  const state = emptyState();
  if (!value || typeof value !== "object" || Array.isArray(value)) return state;
  state.identitySalt = typeof value.identitySalt === "string" && /^[a-f0-9]{64}$/.test(value.identitySalt)
    ? value.identitySalt
    : "";
  for (const [accountId, snapshot] of Object.entries(value.accounts ?? {})) {
    if (validAccountId(accountId)) state.accounts[accountId] = normalizeSnapshot(snapshot);
  }
  for (const [fingerprint, snapshot] of Object.entries(value.detached ?? {})) {
    if (/^[a-f0-9]{64}$/.test(fingerprint)) state.detached[fingerprint] = normalizeSnapshot(snapshot);
  }
  return state;
}

function normalizeSnapshot(value) {
  return {
    usage: normalizeUsage(value?.usage),
    updatedAt: typeof value?.updatedAt === "string" && Number.isFinite(Date.parse(value.updatedAt))
      ? new Date(value.updatedAt).toISOString()
      : new Date(0).toISOString()
  };
}

function normalizeUsage(value) {
  const buckets = Array.isArray(value?.dailyUsageBuckets)
    ? value.dailyUsageBuckets.flatMap((bucket) => {
      if (!/^\d{4}-\d{2}-\d{2}$/.test(bucket?.startDate ?? "") || !Number.isSafeInteger(bucket?.tokens) || bucket.tokens < 0) return [];
      return [{ startDate: bucket.startDate, tokens: bucket.tokens }];
    })
    : [];
  const summary = {};
  for (const field of SUMMARY_FIELDS) summary[field] = publicCount(value?.summary?.[field]);
  return { dailyUsageBuckets: buckets.sort((left, right) => left.startDate.localeCompare(right.startDate)), summary };
}

function mergeUsage(previous, next) {
  const left = normalizeUsage(previous);
  const right = normalizeUsage(next);
  const buckets = new Map();
  for (const bucket of [...left.dailyUsageBuckets, ...right.dailyUsageBuckets]) {
    buckets.set(bucket.startDate, Math.max(buckets.get(bucket.startDate) ?? 0, bucket.tokens));
  }
  const summary = {};
  for (const field of SUMMARY_FIELDS) {
    const values = [left.summary[field], right.summary[field]].filter(Number.isSafeInteger);
    summary[field] = values.length ? Math.max(...values) : null;
  }
  return {
    dailyUsageBuckets: [...buckets.entries()]
      .map(([startDate, tokens]) => ({ startDate, tokens }))
      .sort((leftBucket, rightBucket) => leftBucket.startDate.localeCompare(rightBucket.startDate)),
    summary
  };
}

function identityFingerprint(state, identity) {
  if (!state.identitySalt) state.identitySalt = randomBytes(32).toString("hex");
  return createHmac("sha256", state.identitySalt).update(identity).digest("hex");
}

function normalizeIdentity(value) {
  return typeof value === "string" ? value.trim().toLowerCase() : "";
}

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

function publicCount(value) {
  return Number.isSafeInteger(value) && value >= 0 ? value : null;
}

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);
}
