import {
  createHash,
  createPrivateKey,
  createPublicKey,
  generateKeyPairSync,
  randomBytes as cryptoRandomBytes,
  randomUUID,
} from "node:crypto";
import { mkdir, open, readFile, rename, rm, stat, unlink } from "node:fs/promises";
import { join } from "node:path";
import { isDeepStrictEqual } from "node:util";

import {
  assertCanAddExecutionAccount as assertAccountCapacity,
  getAccountExecutionAccess as accountExecutionAccess,
  getPublicEntitlementStatus,
} from "./entitlement-policy.js";
import { verifyEntitlementToken } from "./entitlement-token.js";

const SCHEMA_VERSION = 1;
const CHALLENGE_LIFETIME_MS = 5 * 60 * 1000;
const MAX_CHALLENGES = 32;

export function createEntitlementStore({
  dataDir,
  publicKeys,
  accountsProvider,
  now = Date.now,
  randomBytes = cryptoRandomBytes,
  developerPro = false,
}) {
  const securityDir = join(dataDir, "security");
  const keyPath = join(securityDir, "entitlement-key.json");
  const statePath = join(securityDir, "entitlement.json");
  const lockPath = join(securityDir, "entitlement.lock");
  let installation = null;
  let currentState = null;

  function time() {
    const value = now();
    const milliseconds = value instanceof Date ? value.getTime() : Number(value);
    if (!Number.isFinite(milliseconds)) throw entitlementError("entitlement_state_unavailable", 500);
    return milliseconds;
  }

  function accounts() {
    const value = accountsProvider?.();
    return Array.isArray(value) ? value : [];
  }

  function requireInitialized() {
    if (!installation || !currentState) throw entitlementError("entitlement_store_uninitialized", 500);
  }

  async function readVerifiedState() {
    const state = await loadState(statePath);
    return verifyPersistedState(state, {
      publicKeys,
      backendKeyThumbprint: installation.backendKeyThumbprint,
      now: time(),
    });
  }

  function status() {
    requireInitialized();
    if (developerPro === true) {
      return {
        tier: "pro",
        state: "active",
        maxAccounts: 5,
        configuredAccounts: accounts().filter((account) => account?.historyOnly !== true).length,
        expiresAt: null,
        graceEndsAt: null,
      };
    }
    return getPublicEntitlementStatus({
      certificate: currentState.claims,
      accounts: accounts(),
      now: time(),
    });
  }

  async function mutate(change) {
    requireInitialized();
    return withFileLock(securityDir, lockPath, async () => {
      const state = await readVerifiedState();
      const result = await change(state);
      await writeJson(securityDir, statePath, "entitlement", state);
      currentState = state;
      return result;
    });
  }

  return {
    async initialize() {
      await withFileLock(securityDir, lockPath, async () => {
        installation = await loadInstallation(keyPath);
        if (!installation) {
          installation = createInstallation();
          await writeJson(securityDir, keyPath, "entitlement-key", installation.record);
        }
        currentState = await readVerifiedState();
        await writeJson(securityDir, statePath, "entitlement", currentState);
      });
    },

    async getStatus() {
      requireInitialized();
      currentState = await readVerifiedState();
      return status();
    },

    async issueChallenge() {
      requireInitialized();
      const issuedAt = time();
      const challenge = Buffer.from(randomBytes(32)).toString("base64url");
      if (!challenge) throw entitlementError("entitlement_challenge_unavailable", 500);
      const challengeHash = hash(challenge);
      await mutate((state) => {
        state.challenges = pruneChallenges(state.challenges, issuedAt);
        state.challenges.push({
          hash: challengeHash,
          issuedAt: new Date(issuedAt).toISOString(),
          expiresAt: new Date(issuedAt + CHALLENGE_LIFETIME_MS).toISOString(),
          consumedAt: null,
        });
        state.challenges = state.challenges.slice(-MAX_CHALLENGES);
      });
      return {
        challenge,
        backendKeyThumbprint: installation.backendKeyThumbprint,
      };
    },

    async activate({ challenge, certificate }) {
      requireInitialized();
      if (typeof challenge !== "string" || challenge.length > 512 || typeof certificate !== "string") {
        throw entitlementError("entitlement_challenge_invalid", 400);
      }
      const challengeHash = hash(challenge);
      const currentTime = time();
      await mutate((state) => {
        const record = state.challenges.find((item) => item.hash === challengeHash);
        if (!record) throw entitlementError("entitlement_challenge_invalid", 400);
        if (record.consumedAt) {
          if (state.certificate === certificate) return;
          throw entitlementError("entitlement_challenge_used", 409);
        }
        if (currentTime >= Date.parse(record.expiresAt)) {
          throw entitlementError("entitlement_challenge_expired", 409);
        }
        const claims = verifyEntitlementToken(certificate, {
          publicKeys,
          backendKeyThumbprint: installation.backendKeyThumbprint,
          challengeHash,
          now: currentTime,
        });
        record.consumedAt = new Date(currentTime).toISOString();
        state.certificate = certificate;
        state.claims = claims;
      });
      return status();
    },

    getAccountExecutionAccess(accountId) {
      return accountExecutionAccess({
        accountId,
        accounts: accounts(),
        entitlement: status(),
      });
    },

    assertCanAddExecutionAccount() {
      return assertAccountCapacity({ accounts: accounts(), entitlement: status() });
    },
  };
}

function createInstallation() {
  const { privateKey, publicKey } = generateKeyPairSync("ed25519");
  const privateDer = privateKey.export({ format: "der", type: "pkcs8" });
  const publicDer = publicKey.export({ format: "der", type: "spki" });
  const backendKeyThumbprint = createHash("sha256").update(publicDer).digest("base64url");
  return {
    backendKeyThumbprint,
    privateKey,
    publicKey,
    record: {
      schemaVersion: SCHEMA_VERSION,
      privateKey: privateDer.toString("base64url"),
      publicKey: publicDer.toString("base64url"),
      backendKeyThumbprint,
    },
  };
}

async function loadInstallation(path) {
  let record;
  try {
    record = JSON.parse(await readFile(path, "utf8"));
  } catch (error) {
    if (error?.code === "ENOENT") return null;
    throw entitlementError("entitlement_key_invalid", 500);
  }
  try {
    if (
      record?.schemaVersion !== SCHEMA_VERSION
      || typeof record.privateKey !== "string"
      || typeof record.publicKey !== "string"
      || typeof record.backendKeyThumbprint !== "string"
    ) throw new Error();
    const privateDer = Buffer.from(record.privateKey, "base64url");
    const publicDer = Buffer.from(record.publicKey, "base64url");
    const privateKey = createPrivateKey({ key: privateDer, format: "der", type: "pkcs8" });
    const publicKey = createPublicKey({ key: publicDer, format: "der", type: "spki" });
    const thumbprint = createHash("sha256").update(publicDer).digest("base64url");
    if (privateKey.asymmetricKeyType !== "ed25519" || publicKey.asymmetricKeyType !== "ed25519") throw new Error();
    if (thumbprint !== record.backendKeyThumbprint) throw new Error();
    return { backendKeyThumbprint: thumbprint, privateKey, publicKey, record };
  } catch {
    throw entitlementError("entitlement_key_invalid", 500);
  }
}

async function loadState(path) {
  let state;
  try {
    state = JSON.parse(await readFile(path, "utf8"));
  } catch (error) {
    if (error?.code === "ENOENT") {
      return { schemaVersion: SCHEMA_VERSION, certificate: null, claims: null, challenges: [] };
    }
    throw entitlementError("entitlement_state_invalid", 500);
  }
  if (
    state?.schemaVersion !== SCHEMA_VERSION
    || !Array.isArray(state.challenges)
    || !(state.certificate === null || typeof state.certificate === "string")
    || !(state.claims === null || typeof state.claims === "object")
  ) throw entitlementError("entitlement_state_invalid", 500);
  return state;
}

function verifyPersistedState(state, { publicKeys, backendKeyThumbprint, now }) {
  if (state.certificate === null) {
    if (state.claims !== null) throw entitlementError("entitlement_state_invalid", 500);
    return state;
  }
  if (!state.claims || typeof state.claims !== "object" || Array.isArray(state.claims)) {
    throw entitlementError("entitlement_state_invalid", 500);
  }
  try {
    const expiresAt = Number.isSafeInteger(state.claims.exp) ? state.claims.exp * 1000 : now;
    const claims = verifyEntitlementToken(state.certificate, {
      publicKeys,
      backendKeyThumbprint,
      challengeHash: state.claims.challengeHash,
      now: Math.min(now, expiresAt - 1),
    });
    if (!isDeepStrictEqual(claims, state.claims)) throw new Error();
    return state;
  } catch {
    throw entitlementError("entitlement_state_invalid", 500);
  }
}

function pruneChallenges(challenges, now) {
  return challenges
    .filter((record) => record && typeof record.hash === "string" && Date.parse(record.expiresAt) > now)
    .slice(-MAX_CHALLENGES);
}

async function writeJson(directory, path, prefix, value) {
  await mkdir(directory, { recursive: true, mode: 0o700 });
  const temporary = join(directory, `${prefix}.${process.pid}.${randomUUID()}.pending`);
  let handle;
  try {
    handle = await open(temporary, "wx", 0o600);
    await handle.writeFile(JSON.stringify(value));
    await handle.sync();
    await handle.close();
    handle = null;
    await rename(temporary, path);
  } finally {
    await handle?.close().catch(() => {});
    await rm(temporary, { force: true }).catch(() => {});
  }
}

async function withFileLock(directory, lockPath, operation) {
  await mkdir(directory, { recursive: true, mode: 0o700 });
  const deadline = Date.now() + 3000;
  let handle;
  while (!handle) {
    try {
      handle = await open(lockPath, "wx", 0o600);
    } catch (error) {
      if (error?.code !== "EEXIST") throw entitlementError("entitlement_state_unavailable", 500);
      await removeStaleLock(lockPath);
      if (Date.now() >= deadline) throw entitlementError("entitlement_state_busy", 503);
      await new Promise((resolve) => setTimeout(resolve, 20));
    }
  }
  try {
    return await operation();
  } finally {
    await handle.close().catch(() => {});
    await unlink(lockPath).catch(() => {});
  }
}

async function removeStaleLock(lockPath) {
  try {
    const details = await stat(lockPath);
    if (Date.now() - details.mtimeMs > 30000) await unlink(lockPath);
  } catch {
    // Another process released the lock.
  }
}

function hash(value) {
  return createHash("sha256").update(value).digest("base64url");
}

function entitlementError(code, status) {
  const messages = {
    entitlement_challenge_expired: "The entitlement challenge has expired.",
    entitlement_challenge_invalid: "The entitlement challenge is invalid.",
    entitlement_challenge_unavailable: "The entitlement challenge is unavailable.",
    entitlement_challenge_used: "The entitlement challenge has already been used.",
    entitlement_key_invalid: "The entitlement key is unavailable.",
    entitlement_state_busy: "The entitlement state is busy.",
    entitlement_state_invalid: "The entitlement state is unavailable.",
    entitlement_state_unavailable: "The entitlement state is unavailable.",
    entitlement_store_uninitialized: "The entitlement store is unavailable.",
  };
  const error = new Error(messages[code] ?? "The entitlement operation failed.");
  error.status = status;
  error.code = code;
  error.safeLogCode = code;
  return error;
}
