import fs from "node:fs/promises";
import path from "node:path";

const EMPTY_STATE = () => ({ version: 1, cursor: 0, grants: {}, accounting: [] });

export class EdgeState {
  constructor(stateDir) {
    this.stateDir = stateDir;
    this.file = path.join(stateDir, "state.json");
    this.data = EMPTY_STATE();
  }

  async load() {
    await fs.mkdir(this.stateDir, { recursive: true, mode: 0o700 });
    try {
      const parsed = JSON.parse(await fs.readFile(this.file, "utf8"));
      this.data = {
        ...EMPTY_STATE(),
        ...parsed,
        grants: parsed.grants && typeof parsed.grants === "object" ? parsed.grants : {},
        accounting: Array.isArray(parsed.accounting) ? parsed.accounting : [],
      };
    } catch (error) {
      if (error.code !== "ENOENT") throw error;
    }
    this.pruneExpired();
    await this.save();
  }

  async save() {
    const temp = `${this.file}.${process.pid}.tmp`;
    await fs.writeFile(temp, `${JSON.stringify(this.data)}\n`, { mode: 0o600 });
    await fs.rename(temp, this.file);
  }

  pruneExpired(now = Date.now()) {
    for (const [deviceHash, grant] of Object.entries(this.data.grants)) {
      if (!grant?.leaseExpiresAt || new Date(grant.leaseExpiresAt).getTime() <= now) delete this.data.grants[deviceHash];
    }
  }

  getGrant(deviceHash, now = Date.now()) {
    const grant = this.data.grants[deviceHash];
    if (!grant) return null;
    if (new Date(grant.leaseExpiresAt).getTime() <= now || new Date(grant.expiresAt).getTime() <= now) {
      delete this.data.grants[deviceHash];
      return null;
    }
    return grant;
  }

  async applySync(payload, expectedHotspotId) {
    if (!payload?.ok || payload.hotspotId !== expectedHotspotId) throw new Error("Cloud returned an invalid hotspot sync response");
    const next = {};
    for (const grant of Array.isArray(payload.authorizations) ? payload.authorizations : []) {
      if (grant?.hotspotId !== expectedHotspotId || !/^[a-f0-9]{64}$/.test(grant?.deviceHash || "")) continue;
      if (!grant.grant || grant.status !== "active") continue;
      if (new Date(grant.leaseExpiresAt).getTime() <= Date.now()) continue;
      next[grant.deviceHash] = {
        version: grant.version,
        voucherId: grant.voucherId,
        hotspotId: grant.hotspotId,
        deviceHash: grant.deviceHash,
        expiresAt: grant.expiresAt,
        leaseExpiresAt: grant.leaseExpiresAt,
        sessionTimeout: grant.sessionTimeout,
        downloadKbps: grant.downloadKbps,
        uploadKbps: grant.uploadKbps,
        dataLimitMb: grant.dataLimitMb,
        status: grant.status,
        grant: grant.grant,
      };
    }
    this.data.grants = next;
    this.data.cursor = Math.max(this.data.cursor, Number(payload.cursor) || 0);
    await this.save();
  }

  async queueAccounting(event) {
    this.data.accounting.push(event);
    if (this.data.accounting.length > 10_000) this.data.accounting.splice(0, this.data.accounting.length - 10_000);
    await this.save();
  }

  accountingBatch(size = 100) {
    return this.data.accounting.slice(0, size);
  }

  async acknowledgeAccounting(count) {
    this.data.accounting.splice(0, count);
    await this.save();
  }
}
