import { prisma } from "../db/prisma.js";
import {
  MIN_OVERALL_F1,
  runTechStepEvalSuite,
} from "../lib/recipe-matching/tech-step-eval-runner.js";
import { backfillTechSteps } from "./backfill-tech-steps.js";

/** Parses `--applied=1,2,3`/`--rejected=4,5` from argv into id arrays — both optional, both empty by default (a run with neither flag only re-gates + backfills, doesn't touch any suggestion's status). */
function parseSuggestionIds(flag: "applied" | "rejected"): number[] {
  const prefix = `--${flag}=`;
  const arg = process.argv.find((value) => value.startsWith(prefix));
  if (arg === undefined) return [];
  return arg
    .slice(prefix.length)
    .split(",")
    .map((value) => value.trim())
    .filter((value) => value.length > 0)
    .map((value) => {
      const id = Number(value);
      if (!Number.isInteger(id)) {
        throw new Error(`--${flag}: "${value}" is not a valid integer id`);
      }
      return id;
    });
}

/**
 * Maintainer workflow closing the loop on a training-corpus change (see
 * this feature's plan document):
 *
 *   1. A maintainer has already hand-edited
 *      `services/tech-step-intent-service/intent_service/training_data.py`
 *      (informed by `list-pending-training-suggestions.ts`'s report),
 *      decided which `TechStepTrainingSuggestion` ids they incorporated
 *      (`--applied=`) or explicitly discarded (`--rejected=`), **and
 *      restarted `tech-step-intent-service`** so it retrains from the
 *      edited corpus — that service only ever trains once, at its own
 *      startup (see its README), so this script's eval gate below is
 *      meaningless against a service still running the old corpus.
 *   2. This script re-runs the F1 regression gate
 *      ({@link runTechStepEvalSuite} against {@link MIN_OVERALL_F1}) —
 *      refuses to backfill at all if the edited corpus scores worse than
 *      the floor, so a bad edit never reaches every existing recipe.
 *   3. Backfills every `Step`'s `StepTechStep` sequence against the new
 *      corpus ({@link backfillTechSteps}).
 *   4. Marks the given suggestion ids `applied`/`rejected`, so
 *      `list-pending-training-suggestions.ts`'s next report doesn't
 *      surface them again.
 *
 * Usage:
 *
 *   pnpm --filter api exec tsx src/scripts/retrain-tech-steps.ts --applied=12,13 --rejected=14
 *
 * `--applied`/`--rejected` are both optional — omitting both still runs
 * the gate + backfill, just leaves every suggestion's `status` untouched
 * (useful for re-running the backfill alone after a corpus edit made with
 * no suggestions involved at all).
 */
async function retrainTechSteps(): Promise<void> {
  const appliedIds = parseSuggestionIds("applied");
  const rejectedIds = parseSuggestionIds("rejected");

  console.info("Evaluating the current classifier against the labeled evaluation set...");
  const { overall } = await runTechStepEvalSuite();
  console.info(
    `F1 ${overall.f1.toFixed(3)} (precision ${overall.precision.toFixed(3)}, recall ${overall.recall.toFixed(3)})`,
  );
  if (overall.f1 < MIN_OVERALL_F1) {
    throw new Error(
      `Aggregate F1 ${overall.f1.toFixed(3)} is below the ${MIN_OVERALL_F1} regression floor — refusing to backfill. Revert or fix the corpus change and re-run.`,
    );
  }

  const { total, changed } = await backfillTechSteps();
  console.info(`Backfilled ${changed}/${total} step(s).`);

  if (appliedIds.length > 0) {
    // `updateMany`'s own `count` (rows actually matched/updated), not
    // `appliedIds.length` (what was merely *asked for*) — an id that
    // doesn't exist (typo, already-processed id) would otherwise log a
    // success count that silently doesn't match what really changed.
    const { count } = await prisma.techStepTrainingSuggestion.updateMany({
      where: { id: { in: appliedIds } },
      data: { status: "applied" },
    });
    console.info(`Marked ${count}/${appliedIds.length} suggestion(s) as applied.`);
  }
  if (rejectedIds.length > 0) {
    const { count } = await prisma.techStepTrainingSuggestion.updateMany({
      where: { id: { in: rejectedIds } },
      data: { status: "rejected" },
    });
    console.info(`Marked ${count}/${rejectedIds.length} suggestion(s) as rejected.`);
  }
}

retrainTechSteps()
  .then(() => prisma.$disconnect())
  .catch(async (err) => {
    console.error(err);
    await prisma.$disconnect();
    process.exit(1);
  });
