import { createHash, randomBytes } from "node:crypto";
import { createReadStream } from "node:fs";
import {
  lstat,
  mkdir,
  open,
  readFile,
  readdir,
  realpath,
  rename,
  rm
} from "node:fs/promises";
import path from "node:path";
import { defaultRunrailEntitlementDirectory } from "./runrail-local-entitlement-core.mjs";

const TARGET_SCHEMA_VERSION = 3;
const MAX_FILES = 20_000;
const MAX_MANIFEST_BYTES = 8 * 1024 * 1024;
const MIGRATION_DIRECTORY = "native-migration-v3";
const SCHEMA_MARKER = ".runrail-native-state.json";
const JOURNAL_FILE = "journal.json";
const SNAPSHOT_FILE = "pre-migration-manifest.json";

function migrationError(code, message) {
  const error = new Error(message);
  error.code = code;
  return error;
}

function stableValue(value) {
  if (Array.isArray(value)) return value.map(stableValue);
  if (value && typeof value === "object") {
    return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stableValue(value[key])]));
  }
  return value;
}

function stableJson(value) {
  return `${JSON.stringify(stableValue(value), null, 2)}\n`;
}

async function sha256File(filePath) {
  const hash = createHash("sha256");
  await new Promise((resolve, reject) => {
    const stream = createReadStream(filePath);
    stream.on("data", (chunk) => hash.update(chunk));
    stream.on("error", reject);
    stream.on("end", resolve);
  });
  return hash.digest("hex");
}

async function atomicJson(target, value) {
  const body = stableJson(value);
  if (Buffer.byteLength(body) > MAX_MANIFEST_BYTES) {
    throw migrationError("runrail-native-migration-manifest-too-large", "Native migration manifest exceeded its bounded size.");
  }
  const directory = path.dirname(target);
  await mkdir(directory, { recursive: true, mode: 0o700 });
  const temporary = path.join(directory, `.${path.basename(target)}.${randomBytes(12).toString("hex")}.tmp`);
  const handle = await open(temporary, "wx", 0o600);
  try {
    await handle.writeFile(body, "utf8");
    await handle.sync();
  } finally {
    await handle.close();
  }
  await rename(temporary, target);
}

async function regularDirectory(directory, { create = false } = {}) {
  const absolute = path.resolve(directory);
  if (create) await mkdir(absolute, { recursive: true, mode: 0o700 });
  const entry = await lstat(absolute).catch(() => null);
  if (!entry || entry.isSymbolicLink() || !entry.isDirectory()) {
    throw migrationError("runrail-native-migration-root-invalid", "Native migration roots must be ordinary directories.");
  }
  const resolved = await realpath(absolute);
  if (resolved !== absolute) {
    throw migrationError("runrail-native-migration-root-invalid", "Native migration roots must not traverse symbolic links.");
  }
  return absolute;
}

async function collectFiles(root, directory = root, files = []) {
  for (const entry of await readdir(directory, { withFileTypes: true })) {
    if (root === directory && [MIGRATION_DIRECTORY, SCHEMA_MARKER].includes(entry.name)) continue;
    const absolute = path.join(directory, entry.name);
    if (entry.isSymbolicLink()) {
      throw migrationError("runrail-native-migration-symlink-refused", "Native migration refuses symbolic links in managed state.");
    }
    if (entry.isDirectory()) await collectFiles(root, absolute, files);
    else if (entry.isFile()) {
      files.push(absolute);
      if (files.length > MAX_FILES) {
        throw migrationError("runrail-native-migration-file-limit", "Native migration state exceeded its bounded file count.");
      }
    } else {
      throw migrationError("runrail-native-migration-special-file-refused", "Native migration refuses special files in managed state.");
    }
  }
  return files;
}

async function rootSnapshot(name, root) {
  const files = await collectFiles(root);
  const rows = [];
  for (const filePath of files) {
    const entry = await lstat(filePath);
    rows.push({
      path: path.relative(root, filePath).split(path.sep).join("/"),
      byteCount: entry.size,
      sha256: await sha256File(filePath)
    });
  }
  rows.sort((left, right) => Buffer.compare(Buffer.from(left.path), Buffer.from(right.path)));
  return { name, fileCount: rows.length, files: rows };
}

async function buildSnapshot(operatorRoot, entitlementRoot) {
  const roots = [
    await rootSnapshot("operator", operatorRoot),
    await rootSnapshot("entitlement", entitlementRoot)
  ];
  const body = {
    schemaVersion: 1,
    artifactType: "runrail-native-pre-migration-manifest",
    fromVersion: 2,
    toVersion: TARGET_SCHEMA_VERSION,
    roots,
    authority: "local-state-migration-only"
  };
  return {
    ...body,
    checksumSha256: createHash("sha256").update(stableJson(body)).digest("hex")
  };
}

async function assertSnapshotMatches(snapshot, operatorRoot, entitlementRoot) {
  const current = await buildSnapshot(operatorRoot, entitlementRoot);
  if (current.checksumSha256 !== snapshot.checksumSha256) {
    throw migrationError(
      "runrail-native-migration-state-changed",
      "Managed state changed after the native migration snapshot was prepared."
    );
  }
}

function verifySnapshotDocument(snapshot) {
  const { checksumSha256, ...body } = snapshot || {};
  if (
    body.schemaVersion !== 1 ||
    body.artifactType !== "runrail-native-pre-migration-manifest" ||
    body.fromVersion !== 2 ||
    body.toVersion !== TARGET_SCHEMA_VERSION ||
    body.authority !== "local-state-migration-only" ||
    !Array.isArray(body.roots) ||
    !/^[a-f0-9]{64}$/.test(String(checksumSha256 || "")) ||
    createHash("sha256").update(stableJson(body)).digest("hex") !== checksumSha256
  ) {
    throw migrationError("runrail-native-migration-snapshot-invalid", "Native migration snapshot failed verification.");
  }
  return snapshot;
}

async function readJson(target) {
  return JSON.parse(await readFile(target, "utf8"));
}

export async function migrateRunrailNativeState({
  operatorRoot,
  entitlementRoot = defaultRunrailEntitlementDirectory(),
  now = new Date()
}) {
  const managedOperatorRoot = await regularDirectory(operatorRoot, { create: true });
  const managedEntitlementRoot = await regularDirectory(entitlementRoot, { create: true });
  const migrationRoot = path.join(managedOperatorRoot, MIGRATION_DIRECTORY);
  await regularDirectory(migrationRoot, { create: true });
  const markerPath = path.join(managedOperatorRoot, SCHEMA_MARKER);
  const journalPath = path.join(migrationRoot, JOURNAL_FILE);
  const snapshotPath = path.join(migrationRoot, SNAPSHOT_FILE);

  const existingMarker = await lstat(markerPath).catch(() => null);
  if (existingMarker) {
    if (existingMarker.isSymbolicLink() || !existingMarker.isFile()) {
      throw migrationError("runrail-native-migration-marker-invalid", "Native state schema marker must be an ordinary file.");
    }
    const marker = await readJson(markerPath);
    if (
      marker.schemaVersion !== 1 ||
      marker.artifactType !== "runrail-native-state-schema" ||
      marker.stateSchemaVersion !== TARGET_SCHEMA_VERSION ||
      marker.migrationStatus !== "committed"
    ) {
      throw migrationError("runrail-native-migration-marker-invalid", "Native state schema marker is invalid.");
    }
    verifySnapshotDocument(await readJson(snapshotPath));
    return Object.freeze({
      ok: true,
      migrated: false,
      recovered: false,
      stateSchemaVersion: TARGET_SCHEMA_VERSION,
      snapshotPath,
      authority: "local-state-migration-only"
    });
  }

  let snapshot;
  let recoveringPreparedJournal = false;
  const existingJournalEntry = await lstat(journalPath).catch(() => null);
  if (existingJournalEntry) {
    if (existingJournalEntry.isSymbolicLink() || !existingJournalEntry.isFile()) {
      throw migrationError("runrail-native-migration-journal-invalid", "Native migration journal must be an ordinary file.");
    }
    const journal = await readJson(journalPath);
    if (journal.phase === "rolled-back" && journal.fromVersion === TARGET_SCHEMA_VERSION && journal.toVersion === 2) {
      snapshot = verifySnapshotDocument(await readJson(snapshotPath));
      await assertSnapshotMatches(snapshot, managedOperatorRoot, managedEntitlementRoot);
      await rm(migrationRoot, { recursive: true, force: true });
      await regularDirectory(migrationRoot, { create: true });
      snapshot = null;
    } else if (journal.phase !== "prepared" || journal.toVersion !== TARGET_SCHEMA_VERSION) {
      throw migrationError("runrail-native-migration-journal-invalid", "Native migration journal is not recoverable.");
    } else {
      snapshot = verifySnapshotDocument(await readJson(snapshotPath));
      await assertSnapshotMatches(snapshot, managedOperatorRoot, managedEntitlementRoot);
      recoveringPreparedJournal = true;
    }
  }
  if (!snapshot) {
    snapshot = await buildSnapshot(managedOperatorRoot, managedEntitlementRoot);
    await atomicJson(snapshotPath, snapshot);
    await atomicJson(journalPath, {
      schemaVersion: 1,
      artifactType: "runrail-native-migration-journal",
      fromVersion: 2,
      toVersion: TARGET_SCHEMA_VERSION,
      phase: "prepared",
      snapshotChecksumSha256: snapshot.checksumSha256,
      authority: "local-state-migration-only"
    });
  }

  const timestamp = new Date(now).toISOString();
  await atomicJson(markerPath, {
    schemaVersion: 1,
    artifactType: "runrail-native-state-schema",
    stateSchemaVersion: TARGET_SCHEMA_VERSION,
    migrationStatus: "committed",
    migratedAt: timestamp,
    snapshotChecksumSha256: snapshot.checksumSha256,
    authority: "local-state-migration-only"
  });
  await atomicJson(journalPath, {
    schemaVersion: 1,
    artifactType: "runrail-native-migration-journal",
    fromVersion: 2,
    toVersion: TARGET_SCHEMA_VERSION,
    phase: "committed",
    committedAt: timestamp,
    snapshotChecksumSha256: snapshot.checksumSha256,
    authority: "local-state-migration-only"
  });
  return Object.freeze({
    ok: true,
    migrated: true,
    recovered: recoveringPreparedJournal,
    stateSchemaVersion: TARGET_SCHEMA_VERSION,
    snapshotPath,
    authority: "local-state-migration-only"
  });
}

export async function rollbackRunrailNativeStateMigration({ operatorRoot }) {
  const managedOperatorRoot = await regularDirectory(operatorRoot);
  const migrationRoot = await regularDirectory(path.join(managedOperatorRoot, MIGRATION_DIRECTORY));
  const snapshotPath = path.join(migrationRoot, SNAPSHOT_FILE);
  verifySnapshotDocument(await readJson(snapshotPath));
  await rm(path.join(managedOperatorRoot, SCHEMA_MARKER), { force: true });
  await atomicJson(path.join(migrationRoot, JOURNAL_FILE), {
    schemaVersion: 1,
    artifactType: "runrail-native-migration-journal",
    fromVersion: TARGET_SCHEMA_VERSION,
    toVersion: 2,
    phase: "rolled-back",
    snapshotChecksumSha256: (await readJson(snapshotPath)).checksumSha256,
    authority: "local-state-migration-only"
  });
  return Object.freeze({ ok: true, rolledBack: true, stateSchemaVersion: 2 });
}
