/**
 * drift-gen entry: corpus generation, on-disk layout, manifest, and the
 * `remnic bench drift-gen` command implementation (issue #1954).
 *
 * Layout per run:
 *   <out>/dataset.manifest.json
 *   <out>/<seed>/users/<userId>/sessions.jsonl
 *   <out>/<seed>/gold/facts.jsonl
 *   <out>/<seed>/gold/probes.jsonl
 */

import { createHash } from "node:crypto";
import { mkdir, readdir, rename, rm, writeFile } from "node:fs/promises";
import path from "node:path";
import { createSeededRandom } from "../../seeded-random.js";
import { buildCorpusSchedule } from "./schedule.js";
import { renderUserSessions } from "./render.js";
import { validateDriftCorpus } from "./validate.js";
import type {
  DriftGenCorpus,
  DriftGenManifest,
  DriftGenOptions,
  DriftGenResult,
  DriftSession,
  DriftValidationReport,
} from "./types.js";

/**
 * Bump on any change to schedule.ts / render.ts / names.ts: those changes
 * alter generated bytes for existing seeds (dataset runbook, Maintenance 1).
 */
export const DRIFT_GEN_VERSION = "1.0.0";

/**
 * Fixed sentinel so repeated runs with the same seed are byte-identical
 * (dataset convention 4 forbids wall-clock timestamps in generated files).
 */
const MANIFEST_TIMESTAMP = "1970-01-01T00:00:00.000Z";

export const DRIFT_GEN_DEFAULTS = Object.freeze({
  users: 5,
  epochs: 12,
  seed: 11,
  factsPerEpoch: 8,
  driftingRatio: 0.2,
  contradictedRatio: 0.1,
});

export function buildDriftCorpus(
  options: Omit<DriftGenOptions, "outDir">,
): DriftGenCorpus {
  const factsPerEpoch = options.factsPerEpoch ?? DRIFT_GEN_DEFAULTS.factsPerEpoch;
  const driftingRatio = options.driftingRatio ?? DRIFT_GEN_DEFAULTS.driftingRatio;
  const contradictedRatio =
    options.contradictedRatio ?? DRIFT_GEN_DEFAULTS.contradictedRatio;
  const schedule = buildCorpusSchedule({
    users: options.users,
    epochs: options.epochs,
    seed: options.seed,
    factsPerEpoch,
    driftingRatio,
    contradictedRatio,
  });
  const renderRng = createSeededRandom((options.seed ^ 0x5f3759) >>> 0);
  const sessions: DriftSession[] = [];
  for (const user of schedule.users) {
    sessions.push(...renderUserSessions(renderRng, user, options.epochs));
  }
  return { facts: schedule.facts, probes: schedule.probes, sessions };
}

function toJsonl(rows: readonly unknown[]): string {
  return `${rows.map((row) => JSON.stringify(row)).join("\n")}\n`;
}

export async function generateDriftCorpus(
  options: DriftGenOptions,
): Promise<DriftGenResult> {
  const corpus = buildDriftCorpus(options);
  const seedDir = String(options.seed);
  const written = new Map<string, string>();

  written.set(
    path.posix.join(seedDir, "gold", "facts.jsonl"),
    toJsonl(corpus.facts),
  );
  written.set(
    path.posix.join(seedDir, "gold", "probes.jsonl"),
    toJsonl(corpus.probes),
  );
  const sessionsByUser = new Map<string, DriftSession[]>();
  for (const session of corpus.sessions) {
    const list = sessionsByUser.get(session.userId) ?? [];
    list.push(session);
    sessionsByUser.set(session.userId, list);
  }
  for (const [userId, sessions] of [...sessionsByUser.entries()].sort()) {
    written.set(
      path.posix.join(seedDir, "users", userId, "sessions.jsonl"),
      toJsonl(sessions),
    );
  }

  const files: Record<string, string> = {};
  for (const relPath of [...written.keys()].sort()) {
    files[relPath] = createHash("sha256")
      .update(written.get(relPath) as string)
      .digest("hex");
  }

  const manifest: DriftGenManifest = {
    name: "drift-gen-core",
    version: DRIFT_GEN_VERSION,
    generatorVersion: DRIFT_GEN_VERSION,
    seeds: [options.seed],
    counts: {
      users: options.users,
      epochs: options.epochs,
      facts: corpus.facts.length,
      probes: corpus.probes.length,
    },
    generator: {
      factsPerEpoch: options.factsPerEpoch ?? DRIFT_GEN_DEFAULTS.factsPerEpoch,
      driftingRatio: options.driftingRatio ?? DRIFT_GEN_DEFAULTS.driftingRatio,
      contradictedRatio:
        options.contradictedRatio ?? DRIFT_GEN_DEFAULTS.contradictedRatio,
    },
    files,
    createdAt: MANIFEST_TIMESTAMP,
    licenses: [{ source: "synthetic", license: "MIT (repo)" }],
    ...(options.audit ? { audit: options.audit } : {}),
  };

  const stagingDir = path.join(options.outDir, `.staging-${options.seed}`);
  await rm(stagingDir, { recursive: true, force: true });
  for (const [relPath, content] of written) {
    const absPath = path.join(stagingDir, path.relative(seedDir, relPath));
    await mkdir(path.dirname(absPath), { recursive: true });
    await writeFile(absPath, content, "utf8");
  }

  const finalSeedDir = path.join(options.outDir, seedDir);
  const backupDir = path.join(options.outDir, `.backup-${options.seed}-${process.pid}`);
  const staleSeedDirs = (await readdir(options.outDir, { withFileTypes: true }))
    .filter((entry) => entry.isDirectory() && /^\d+$/.test(entry.name) && entry.name !== seedDir)
    .map((entry) => ({
      source: path.join(options.outDir, entry.name),
      backup: path.join(options.outDir, `.backup-stale-${entry.name}-${process.pid}`),
    }));
  const quarantinedStaleDirs: typeof staleSeedDirs = [];
  const manifestPath = path.join(options.outDir, "dataset.manifest.json");
  const manifestStaging = path.join(options.outDir, ".staging-manifest.json");
  const manifestBackup = path.join(options.outDir, `.backup-manifest-${process.pid}.json`);
  let hadPrevious = false;
  let replacementInstalled = false;
  let hadPreviousManifest = false;

  try {
    try {
      await rename(manifestPath, manifestBackup);
      hadPreviousManifest = true;
    } catch (error) {
      if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
    }

    for (const stale of staleSeedDirs) {
      await rename(stale.source, stale.backup);
      quarantinedStaleDirs.push(stale);
    }

    try {
      await rename(finalSeedDir, backupDir);
      hadPrevious = true;
    } catch (error) {
      if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
    }

    await rename(stagingDir, finalSeedDir);
    replacementInstalled = true;
    await writeFile(manifestStaging, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
    await rename(manifestStaging, manifestPath);
  } catch (error) {
    await rm(manifestStaging, { force: true });
    await rm(stagingDir, { recursive: true, force: true });
    if (hadPreviousManifest) {
      await rm(manifestPath, { force: true });
      await rename(manifestBackup, manifestPath);
    }
    if (replacementInstalled) {
      await rm(finalSeedDir, { recursive: true, force: true });
    }
    if (hadPrevious) {
      try {
        await rename(backupDir, finalSeedDir);
      } catch {
        console.error(
          `drift-gen: failed to restore the previous corpus; it is preserved at ${backupDir}`,
        );
      }
    }
    for (let index = quarantinedStaleDirs.length - 1; index >= 0; index -= 1) {
      const stale = quarantinedStaleDirs[index];
      try {
        await rename(stale.backup, stale.source);
      } catch {
        console.error(
          `drift-gen: failed to restore a stale corpus; it is preserved at ${stale.backup}`,
        );
      }
    }
    throw error;
  }

  await Promise.all([
    ...(hadPrevious ? [rm(backupDir, { recursive: true, force: true })] : []),
    ...quarantinedStaleDirs.map((stale) => rm(stale.backup, { recursive: true, force: true })),
    ...(hadPreviousManifest ? [rm(manifestBackup, { force: true })] : []),
  ]);

  return { manifest, files: [...written.keys()].sort() };
}

function renderValidationReport(report: DriftValidationReport): string {
  const lines: string[] = [];
  lines.push(report.ok ? "drift-gen corpus: VALID" : "drift-gen corpus: INVALID");
  const { stats } = report;
  lines.push(
    `  facts=${stats.facts} probes=${stats.probes} sessions=${stats.sessions} users=${stats.users} epochs=${stats.epochs}`,
  );
  lines.push(
    `  factsPerEpochMean=${stats.factsPerEpochMean.toFixed(2)} drifting=${stats.driftingRatio.toFixed(3)} contradicted=${stats.contradictedRatio.toFixed(3)}`,
  );
  lines.push(
    `  probes by category: current=${stats.probesByCategory.current} historical=${stats.probesByCategory.historical} transition=${stats.probesByCategory.transition} aggregation=${stats.probesByCategory.aggregation}`,
  );
  lines.push(
    `  max question/answer leakage: ${(stats.maxQuestionAnswerLeakage * 100).toFixed(0)}%`,
  );
  for (const warning of report.warnings) lines.push(`  warning: ${warning}`);
  for (const error of report.errors) lines.push(`  error: ${error}`);
  return lines.join("\n");
}

export interface DriftGenCliOptions {
  action: "generate" | "validate";
  dir?: string;
  users?: number;
  epochs?: number;
  seed?: number;
  out?: string;
  factsPerEpoch?: number;
  driftingRatio?: number;
  contradictedRatio?: number;
  json?: boolean;
}

export async function runDriftGenCliCommand(
  options: DriftGenCliOptions,
): Promise<{ exitCode: number; output: string }> {
  if (options.action === "validate") {
    if (!options.dir) {
      return {
        exitCode: 1,
        output: "drift-gen validate requires a corpus directory: remnic bench drift-gen validate <dir>",
      };
    }
    const report = await validateDriftCorpus(options.dir);
    return {
      exitCode: report.ok ? 0 : 1,
      output: options.json
        ? JSON.stringify(report, null, 2)
        : renderValidationReport(report),
    };
  }

  if (!options.out) {
    return {
      exitCode: 1,
      output: "drift-gen requires --out <dir> to write the corpus",
    };
  }
  const result = await generateDriftCorpus({
    users: options.users ?? DRIFT_GEN_DEFAULTS.users,
    epochs: options.epochs ?? DRIFT_GEN_DEFAULTS.epochs,
    seed: options.seed ?? DRIFT_GEN_DEFAULTS.seed,
    outDir: options.out,
    factsPerEpoch: options.factsPerEpoch,
    driftingRatio: options.driftingRatio,
    contradictedRatio: options.contradictedRatio,
  });
  if (options.json) {
    return { exitCode: 0, output: JSON.stringify(result.manifest, null, 2) };
  }
  const { counts } = result.manifest;
  return {
    exitCode: 0,
    output: [
      `drift-gen v${DRIFT_GEN_VERSION}: wrote ${result.files.length + 1} files to ${options.out}`,
      `  users=${counts.users} epochs=${counts.epochs} facts=${counts.facts} probes=${counts.probes} seed=${result.manifest.seeds[0]}`,
      `  validate with: remnic bench drift-gen validate ${options.out}`,
    ].join("\n"),
  };
}

export { validateDriftCorpus } from "./validate.js";
export { buildCorpusSchedule, ATTRIBUTE_SPECS } from "./schedule.js";
export type {
  DriftGenCorpus,
  DriftGenManifest,
  DriftGenOptions,
  DriftGenResult,
  DriftGenAuditRecord,
  DriftSession,
  DriftSessionTurn,
  DriftValidationReport,
  DriftValidationStats,
  GoldFact,
  GoldFactKind,
  GoldProbe,
  GoldProbeCategory,
} from "./types.js";
