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

const STORE_VERSION = 1;
const SETUP_TTL_MS = 15 * 60_000;
const NO_STORE_WRITE = Symbol("no-store-write");

function nowIso(now) {
  return new Date(now()).toISOString();
}

function cleanString(value, max = 240) {
  return String(value || "").trim().slice(0, max);
}

function cleanId(value, max = 180) {
  return cleanString(value, max).replace(/[^A-Za-z0-9_.:@-]/g, "");
}

function cleanAvatarUrl(value) {
  const avatarUrl = cleanString(value, 550_000);
  if (!avatarUrl) return "";
  if (/^https:\/\//i.test(avatarUrl) || /^data:image\/(?:png|jpeg|webp|gif);base64,/i.test(avatarUrl)) return avatarUrl;
  return "";
}

function digest(value) {
  return createHash("sha256").update(String(value || ""), "utf8").digest("hex");
}

function digestMatches(actual, expected) {
  const actualBuffer = Buffer.from(String(actual || ""), "hex");
  const expectedBuffer = Buffer.from(String(expected || ""), "hex");
  return actualBuffer.length === expectedBuffer.length
    && actualBuffer.length > 0
    && timingSafeEqual(actualBuffer, expectedBuffer);
}

function randomToken(prefix, bytes = 32) {
  return `${prefix}_${randomBytes(bytes).toString("base64url")}`;
}

function emptyStore() {
  return { schemaVersion: STORE_VERSION, agents: [], setupCodes: [] };
}

function normalizeDevice(value) {
  const id = cleanId(value?.id);
  const foundryTokenHash = cleanString(value?.foundryTokenHash, 128);
  const ontologyTokenHash = cleanString(value?.ontologyTokenHash, 128);
  if (!id || !foundryTokenHash || !ontologyTokenHash) return null;
  return {
    id,
    name: cleanString(value?.name, 160) || "Codex Desktop",
    foundryTokenHash,
    ontologyTokenHash,
    createdAt: cleanString(value?.createdAt, 80),
    lastUsedAt: cleanString(value?.lastUsedAt, 80) || null,
    lastCheckAt: cleanString(value?.lastCheckAt, 80) || null,
    revokedAt: cleanString(value?.revokedAt, 80) || null,
  };
}

function normalizeAgent(value, now) {
  const id = cleanId(value?.id);
  const ownerId = cleanId(value?.ownerId);
  const ownerKey = cleanString(value?.ownerKey, 320);
  if (!id || !ownerId || !ownerKey) return null;
  return {
    id,
    ownerId,
    ownerKey,
    name: cleanString(value?.name, 160) || "Foundry Codex",
    avatarUrl: cleanAvatarUrl(value?.avatarUrl),
    status: value?.status === "revoked" ? "revoked" : "active",
    devices: Array.isArray(value?.devices) ? value.devices.map(normalizeDevice).filter(Boolean) : [],
    createdAt: cleanString(value?.createdAt, 80) || nowIso(now),
    updatedAt: cleanString(value?.updatedAt, 80) || nowIso(now),
    revokedAt: cleanString(value?.revokedAt, 80) || null,
  };
}

function normalizeSetupCode(value) {
  const codeHash = cleanString(value?.codeHash, 128);
  const agentId = cleanId(value?.agentId);
  const ownerKey = cleanString(value?.ownerKey, 320);
  if (!codeHash || !agentId || !ownerKey) return null;
  return {
    codeHash,
    agentId,
    ownerKey,
    createdAt: cleanString(value?.createdAt, 80),
    expiresAt: cleanString(value?.expiresAt, 80),
    redeemedAt: cleanString(value?.redeemedAt, 80) || null,
    redeemedDeviceId: cleanId(value?.redeemedDeviceId) || null,
  };
}

function normalizeStore(value, now) {
  return {
    schemaVersion: STORE_VERSION,
    agents: Array.isArray(value?.agents) ? value.agents.map((item) => normalizeAgent(item, now)).filter(Boolean) : [],
    setupCodes: Array.isArray(value?.setupCodes) ? value.setupCodes.map(normalizeSetupCode).filter(Boolean) : [],
  };
}

function publicDevice(device) {
  return {
    id: device.id,
    name: device.name,
    createdAt: device.createdAt,
    lastUsedAt: device.lastUsedAt,
    lastCheckAt: device.lastCheckAt,
    revokedAt: device.revokedAt,
  };
}

function publicAgent(agent) {
  return {
    id: agent.id,
    ownerId: agent.ownerId,
    name: agent.name,
    avatarUrl: agent.avatarUrl || null,
    status: agent.status,
    devices: agent.devices.map(publicDevice),
    deviceCount: agent.devices.filter((device) => !device.revokedAt).length,
    lastUsedAt: agent.devices.map((device) => device.lastUsedAt).filter(Boolean).sort().at(-1) || null,
    lastCheckAt: agent.devices.map((device) => device.lastCheckAt).filter(Boolean).sort().at(-1) || null,
    createdAt: agent.createdAt,
    updatedAt: agent.updatedAt,
    revokedAt: agent.revokedAt,
  };
}

export function createFoundryAgentStore({ dataRoot, now = Date.now } = {}) {
  if (!dataRoot) throw new Error("foundry_agent_data_root_required");
  const storePath = join(dataRoot, "agents.json");
  const auditPath = join(dataRoot, "audit.ndjson");
  let mutationQueue = Promise.resolve();
  let auditQueue = Promise.resolve();

  async function readStore() {
    try {
      return normalizeStore(JSON.parse(await readFile(storePath, "utf8")), now);
    } catch (error) {
      if (error?.code === "ENOENT") return emptyStore();
      throw error;
    }
  }

  async function writeStore(store) {
    await mkdir(dataRoot, { recursive: true });
    const tempPath = `${storePath}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
    await writeFile(tempPath, `${JSON.stringify(normalizeStore(store, now), null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
    await rename(tempPath, storePath);
  }

  async function mutate(action) {
    const run = mutationQueue.then(async () => {
      const store = await readStore();
      const result = await action(store);
      if (result !== NO_STORE_WRITE) await writeStore(store);
      return result;
    });
    mutationQueue = run.catch(() => undefined);
    return run;
  }

  async function audit(event) {
    const run = auditQueue.then(async () => {
      const record = {
        schemaVersion: "nodedc.module-foundry.agent-audit.v1",
        at: nowIso(now),
        ...event,
      };
      await mkdir(dataRoot, { recursive: true });
      const current = await readFile(auditPath, "utf8").catch((error) => error?.code === "ENOENT" ? "" : Promise.reject(error));
      const lines = `${current}${JSON.stringify(record)}\n`.trimEnd().split("\n").slice(-2_000);
      const tempPath = `${auditPath}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
      await writeFile(tempPath, `${lines.join("\n")}\n`, { encoding: "utf8", mode: 0o600 });
      await rename(tempPath, auditPath);
    });
    auditQueue = run.catch(() => undefined);
    return run;
  }

  async function listAgents(ownerKey) {
    const normalizedOwner = cleanString(ownerKey, 320);
    const store = await readStore();
    return store.agents.filter((agent) => agent.ownerKey === normalizedOwner).map(publicAgent);
  }

  async function createAgent({ ownerId, ownerKey, name, avatarUrl }) {
    const normalizedOwnerId = cleanId(ownerId);
    const normalizedOwnerKey = cleanString(ownerKey, 320);
    if (!normalizedOwnerId || !normalizedOwnerKey) throw new Error("foundry_agent_owner_required");
    const agent = await mutate(async (store) => {
      const createdAt = nowIso(now);
      const next = {
        id: randomToken("foundry_agent", 12),
        ownerId: normalizedOwnerId,
        ownerKey: normalizedOwnerKey,
        name: cleanString(name, 160) || "Foundry Codex",
        avatarUrl: cleanAvatarUrl(avatarUrl),
        status: "active",
        devices: [],
        createdAt,
        updatedAt: createdAt,
        revokedAt: null,
      };
      store.agents.push(next);
      return publicAgent(next);
    });
    await audit({ event: "agent_created", ownerId: normalizedOwnerId, agentId: agent.id, outcome: "ok" });
    return agent;
  }

  async function updateAgent(ownerKey, agentId, input = {}) {
    const normalizedOwner = cleanString(ownerKey, 320);
    const normalizedId = cleanId(agentId);
    const agent = await mutate(async (store) => {
      const target = store.agents.find((item) => item.id === normalizedId && item.ownerKey === normalizedOwner);
      if (!target) throw new Error("foundry_agent_not_found");
      if (target.status === "revoked") throw new Error("foundry_agent_revoked");
      if (Object.hasOwn(input, "name")) target.name = cleanString(input.name, 160) || target.name;
      if (Object.hasOwn(input, "avatarUrl")) target.avatarUrl = cleanAvatarUrl(input.avatarUrl);
      target.updatedAt = nowIso(now);
      return publicAgent(target);
    });
    await audit({ event: "agent_updated", agentId: agent.id, outcome: "ok" });
    return agent;
  }

  async function revokeAgent(ownerKey, agentId) {
    const normalizedOwner = cleanString(ownerKey, 320);
    const normalizedId = cleanId(agentId);
    const agent = await mutate(async (store) => {
      const target = store.agents.find((item) => item.id === normalizedId && item.ownerKey === normalizedOwner);
      if (!target) throw new Error("foundry_agent_not_found");
      const revokedAt = target.revokedAt || nowIso(now);
      target.status = "revoked";
      target.revokedAt = revokedAt;
      target.updatedAt = revokedAt;
      target.devices.forEach((device) => { device.revokedAt = device.revokedAt || revokedAt; });
      return publicAgent(target);
    });
    await audit({ event: "agent_revoked", agentId: agent.id, outcome: "ok" });
    return agent;
  }

  async function issueSetupCode(ownerKey, agentId) {
    const normalizedOwner = cleanString(ownerKey, 320);
    const normalizedId = cleanId(agentId);
    const code = randomToken("fnd_setup", 24);
    const createdAtMs = now();
    const expiresAt = new Date(createdAtMs + SETUP_TTL_MS).toISOString();
    await mutate(async (store) => {
      const agent = store.agents.find((item) => item.id === normalizedId && item.ownerKey === normalizedOwner);
      if (!agent) throw new Error("foundry_agent_not_found");
      if (agent.status !== "active") throw new Error("foundry_agent_revoked");
      store.setupCodes = store.setupCodes.filter((item) => Date.parse(item.expiresAt || "") > createdAtMs && !item.redeemedAt);
      store.setupCodes.push({
        codeHash: digest(code),
        agentId: agent.id,
        ownerKey: agent.ownerKey,
        createdAt: new Date(createdAtMs).toISOString(),
        expiresAt,
        redeemedAt: null,
        redeemedDeviceId: null,
      });
    });
    await audit({ event: "setup_code_issued", agentId: normalizedId, outcome: "ok" });
    return { code, expiresAt };
  }

  async function redeemSetupCode(code, deviceName) {
    const codeHash = digest(code);
    const foundryToken = randomToken("fnda", 36);
    const ontologyToken = randomToken("fndo", 36);
    const result = await mutate(async (store) => {
      const setup = store.setupCodes.find((item) => item.codeHash === codeHash);
      if (!setup) throw new Error("setup_code_invalid");
      if (setup.redeemedAt) throw new Error("setup_code_already_redeemed");
      if (Date.parse(setup.expiresAt || "") <= now()) throw new Error("setup_code_expired");
      const agent = store.agents.find((item) => item.id === setup.agentId && item.ownerKey === setup.ownerKey);
      if (!agent || agent.status !== "active") throw new Error("foundry_agent_revoked");
      const createdAt = nowIso(now);
      const device = {
        id: randomToken("foundry_device", 12),
        name: cleanString(deviceName, 160) || "Codex Desktop",
        foundryTokenHash: digest(foundryToken),
        ontologyTokenHash: digest(ontologyToken),
        createdAt,
        lastUsedAt: null,
        lastCheckAt: null,
        revokedAt: null,
      };
      agent.devices.push(device);
      agent.updatedAt = createdAt;
      setup.redeemedAt = createdAt;
      setup.redeemedDeviceId = device.id;
      return { agent: publicAgent(agent), device: publicDevice(device) };
    });
    await audit({ event: "setup_code_redeemed", agentId: result.agent.id, deviceId: result.device.id, outcome: "ok" });
    return { ...result, foundryToken, ontologyToken };
  }

  async function authenticate(kind, token, { check = false } = {}) {
    const tokenHash = digest(token);
    return mutate(async (store) => {
      for (const agent of store.agents) {
        if (agent.status !== "active") continue;
        for (const device of agent.devices) {
          const expected = kind === "ontology" ? device.ontologyTokenHash : device.foundryTokenHash;
          if (!digestMatches(expected, tokenHash) || device.revokedAt) continue;
          const at = nowIso(now);
          device.lastUsedAt = at;
          if (check) device.lastCheckAt = at;
          agent.updatedAt = at;
          return {
            actor: { actorId: agent.ownerId, ownerKey: agent.ownerKey },
            agent: publicAgent(agent),
            device: publicDevice(device),
          };
        }
      }
      return NO_STORE_WRITE;
    });
  }

  return {
    listAgents,
    createAgent,
    updateAgent,
    revokeAgent,
    issueSetupCode,
    redeemSetupCode,
    authenticateFoundryToken: async (token, options) => {
      const result = await authenticate("foundry", token, options);
      return result === NO_STORE_WRITE ? null : result;
    },
    authenticateOntologyToken: async (token, options) => {
      const result = await authenticate("ontology", token, options);
      return result === NO_STORE_WRITE ? null : result;
    },
    audit,
  };
}
