import { randomBytes } from "node:crypto";
import {
  mkdir,
  open,
  readFile,
  rename,
  unlink,
  writeFile,
} from "node:fs/promises";
import { dirname } from "node:path";
import { canonicalize } from "../../../schema/src/canonical.js";
import { storeLayout } from "./layout.js";

const LOCK_RETRY_DELAY_MS = 10;
const LOCK_MAX_ATTEMPTS = 1_000;

export class StoreConflictError extends Error {
  readonly preservedInputs: readonly unknown[];
  readonly conflictArtifacts: readonly string[];

  constructor(
    message: string,
    config: {
      preservedInputs: readonly unknown[];
      conflictArtifacts: readonly string[];
    },
  ) {
    super(message);
    this.name = "StoreConflictError";
    this.preservedInputs = config.preservedInputs;
    this.conflictArtifacts = config.conflictArtifacts;
  }
}

interface GenerationState {
  generation: number;
}

function parseGenerationState(raw: string, path: string): GenerationState {
  let parsed: unknown;
  try {
    parsed = JSON.parse(raw) as unknown;
  } catch (error) {
    throw new Error(`invalid generation state at ${path}`, { cause: error });
  }

  if (
    typeof parsed !== "object" ||
    parsed === null ||
    typeof (parsed as GenerationState).generation !== "number" ||
    !Number.isInteger((parsed as GenerationState).generation) ||
    (parsed as GenerationState).generation < 0
  ) {
    throw new Error(`invalid generation state at ${path}`);
  }

  return parsed as GenerationState;
}

async function readGenerationState(root: string): Promise<GenerationState> {
  const layout = storeLayout(root);
  try {
    const raw = await readFile(layout.generation(), "utf8");
    return parseGenerationState(raw, layout.generation());
  } catch (error) {
    if ((error as NodeJS.ErrnoException).code === "ENOENT") {
      return { generation: 0 };
    }
    throw error;
  }
}

async function writeGenerationState(
  root: string,
  generation: number,
): Promise<void> {
  const layout = storeLayout(root);
  const generationPath = layout.generation();
  await mkdir(dirname(generationPath), { recursive: true });
  const payload = canonicalize({ generation });
  const tempPath = `${generationPath}.${String(process.pid)}-${randomBytes(4).toString("hex")}.tmp`;
  await writeFile(tempPath, payload, { encoding: "utf8" });
  const handle = await open(tempPath, "r");
  try {
    await handle.sync();
  } finally {
    await handle.close();
  }
  await rename(tempPath, generationPath);
}

export async function readStoreGeneration(root: string): Promise<number> {
  const state = await readGenerationState(root);
  return state.generation;
}

async function persistConflictArtifacts(
  root: string,
  inputs: readonly unknown[],
): Promise<string[]> {
  const layout = storeLayout(root);
  await mkdir(layout.conflictsDir(), { recursive: true });
  const stamp = `${String(Date.now())}-${randomBytes(4).toString("hex")}`;
  const artifactPaths: string[] = [];

  for (const [index, input] of inputs.entries()) {
    const artifactPath = `${layout.conflictsDir()}/conflict-${stamp}-${String(index)}.json`;
    await writeFile(artifactPath, canonicalize(input), { encoding: "utf8" });
    artifactPaths.push(artifactPath);
  }

  return artifactPaths;
}

export async function compareAndSwapGeneration(
  root: string,
  expectedGeneration: number,
  nextGeneration: number,
  conflictInputs: readonly unknown[],
): Promise<void> {
  if (!Number.isInteger(nextGeneration) || nextGeneration < 0) {
    throw new Error("nextGeneration must be a non-negative integer");
  }

  const current = await readGenerationState(root);
  if (current.generation !== expectedGeneration) {
    const conflictArtifacts = await persistConflictArtifacts(root, conflictInputs);
    throw new StoreConflictError(
      `Store generation conflict: expected ${String(expectedGeneration)} but found ${String(current.generation)}. Retry required.`,
      {
        preservedInputs: conflictInputs,
        conflictArtifacts,
      },
    );
  }

  await writeGenerationState(root, nextGeneration);
}

async function acquireStoreLock(lockPath: string): Promise<() => Promise<void>> {
  await mkdir(dirname(lockPath), { recursive: true });

  for (let attempt = 0; attempt < LOCK_MAX_ATTEMPTS; attempt += 1) {
    try {
      const handle = await open(lockPath, "wx");
      try {
        await handle.writeFile(
          JSON.stringify({
            pid: process.pid,
            acquiredAt: new Date().toISOString(),
          }),
          "utf8",
        );
      } finally {
        await handle.close();
      }

      return async () => {
        await unlink(lockPath).catch(() => undefined);
      };
    } catch (error) {
      if ((error as NodeJS.ErrnoException).code !== "EEXIST") {
        throw error;
      }
      await new Promise((resolve) => {
        setTimeout(resolve, LOCK_RETRY_DELAY_MS);
      });
    }
  }

  throw new Error(`timed out acquiring store lock at ${lockPath}`);
}

export async function withStoreLock<T>(
  root: string,
  fn: () => Promise<T>,
): Promise<T> {
  const layout = storeLayout(root);
  await mkdir(layout.root, { recursive: true });
  const release = await acquireStoreLock(layout.lock());
  try {
    return await fn();
  } finally {
    await release();
  }
}
