import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';

import { AppDataSource } from '../data-source';
import { TRAIN_RUN_PAIRS, toExportRun } from '../modules/wagons/train-runs.const';

/**
 * Update wagon run numbers from a roster file — the tool for making the DB match
 * the operator's sheet.
 *
 *   pnpm seed:wagon-runs <file.csv> [--apply]
 *
 * CSV: two columns, header optional. Either half of a run pair is accepted, so
 * "8001" and "8002" both mean the same train.
 *
 *   wagon_number,run
 *   ER0744,8001
 *   ER0458,8102
 *
 * FULL REPLACEMENT: wagons absent from the file have their runs cleared, so the
 * DB ends up matching the file exactly rather than accumulating stale rows.
 *
 * Dry run by default — it validates and prints what would change. Nothing is
 * written without `--apply`. Validation is fatal on: an unknown run, a wagon not
 * in the database, or the same wagon claimed by two runs (a wagon holds one run,
 * so a double-booking has no correct answer and must be fixed in the sheet).
 */
interface Row {
  line: number;
  wagonNumber: string;
  exportRun: string;
}

function parseCsv(path: string) {
  const text = readFileSync(path, 'utf8');
  const rows: Row[] = [];
  const unknownRuns: string[] = [];

  text.split(/\r?\n/).forEach((raw, i) => {
    const line = i + 1;
    const trimmed = raw.trim();
    if (!trimmed || trimmed.startsWith('#')) return;

    const [rawWagon = '', rawRun = ''] = trimmed.split(',').map((c) => c.trim());
    // Skip a header row without needing it to be declared.
    if (/wagon/i.test(rawWagon) && /run|train/i.test(rawRun)) return;
    if (!rawWagon || !rawRun) {
      throw new Error(`line ${line}: expected "wagon_number,run", got "${trimmed}"`);
    }

    const exportRun = toExportRun(rawRun);
    if (!exportRun) {
      unknownRuns.push(`line ${line}: "${rawRun}" (wagon ${rawWagon})`);
      return;
    }
    rows.push({ line, wagonNumber: rawWagon.toUpperCase(), exportRun });
  });

  return { rows, unknownRuns };
}

async function updateWagonRuns() {
  const [fileArg, ...flags] = process.argv.slice(2);
  const apply = flags.includes('--apply');

  if (!fileArg) {
    console.error('usage: pnpm seed:wagon-runs <file.csv> [--apply]');
    process.exit(2);
  }

  const path = resolve(process.cwd(), fileArg);
  const { rows, unknownRuns } = parseCsv(path);

  // A wagon in two runs cannot be represented — surface every instance rather
  // than silently keeping whichever line happened to come first.
  const seen = new Map<string, Row>();
  const doubleBooked: string[] = [];
  for (const row of rows) {
    const prior = seen.get(row.wagonNumber);
    if (prior && prior.exportRun !== row.exportRun) {
      doubleBooked.push(
        `${row.wagonNumber}: run ${prior.exportRun} (line ${prior.line}) vs ${row.exportRun} (line ${row.line})`,
      );
      continue;
    }
    if (!prior) seen.set(row.wagonNumber, row);
  }

  await AppDataSource.initialize();
  try {
    const wagonNumbers = [...seen.keys()];
    const existing: Array<{ wagon_number: string }> = wagonNumbers.length
      ? await AppDataSource.query(
          `SELECT wagon_number FROM freight.wagons
            WHERE deleted_at IS NULL AND wagon_number = ANY($1::text[]);`,
          [wagonNumbers],
        )
      : [];
    const known = new Set(existing.map((r) => r.wagon_number));
    const missing = wagonNumbers.filter((w) => !known.has(w));

    const problems = [
      ...unknownRuns.map((u) => `unknown run  ${u}`),
      ...doubleBooked.map((d) => `double-booked  ${d}`),
      ...missing.map((m) => `not in database  ${m}`),
    ];

    const perRun = new Map<string, number>();
    for (const row of seen.values()) {
      if (known.has(row.wagonNumber)) {
        perRun.set(row.exportRun, (perRun.get(row.exportRun) ?? 0) + 1);
      }
    }

    console.log(`\nFile: ${path}`);
    console.log(`Rows read: ${rows.length + unknownRuns.length} | assignable: ${known.size}`);
    console.table(
      Object.keys(TRAIN_RUN_PAIRS).map((exportRun) => ({
        export_run: exportRun,
        import_run: TRAIN_RUN_PAIRS[exportRun],
        wagons: perRun.get(exportRun) ?? 0,
      })),
    );

    if (problems.length) {
      console.error(`\n${problems.length} problem(s) — nothing was written:`);
      problems.forEach((p) => console.error(`  ${p}`));
      console.error('\nFix these in the source sheet, then re-run.');
      process.exit(1);
    }

    if (!apply) {
      console.log('\nDry run — no changes written. Re-run with --apply to write.');
      return;
    }

    await AppDataSource.transaction(async (manager) => {
      // Full replacement: clear first so a wagon dropped from the sheet does not
      // keep a run it no longer has.
      await manager.query(`
        UPDATE freight.wagons
        SET export_train_number = NULL, import_train_number = NULL
        WHERE export_train_number IS NOT NULL;
      `);

      for (const exportRun of new Set([...seen.values()].map((r) => r.exportRun))) {
        const wagons = [...seen.values()]
          .filter((r) => r.exportRun === exportRun)
          .map((r) => r.wagonNumber);
        await manager.query(
          `UPDATE freight.wagons
              SET export_train_number = $1,
                  import_train_number = $2,
                  updated_at          = now()
            WHERE wagon_number = ANY($3::text[]);`,
          [exportRun, TRAIN_RUN_PAIRS[exportRun], wagons],
        );
      }
    });

    const [totals] = await AppDataSource.query(`
      SELECT COUNT(*) FILTER (WHERE export_train_number IS NOT NULL)::int AS on_a_run
      FROM freight.wagons WHERE deleted_at IS NULL;
    `);
    console.log(`\nApplied. ${totals.on_a_run} wagons now on a run.`);
  } finally {
    await AppDataSource.destroy();
  }
}

updateWagonRuns().catch((error) => {
  console.error('Failed to update wagon runs:', error instanceof Error ? error.message : error);
  process.exit(1);
});
