import { env } from "./config.js";

/**
 * Thin fetch wrapper around `apps/api`'s `/internal/tech-steps/*` and
 * `/reference/tech-steps` — the only two surfaces this worker ever talks
 * to (see `tech-step-worker.service.ts`/`tech-step-worker.routes.ts` on
 * that side). No Prisma client, no direct database access at all: every
 * read/write goes through here, over HTTP, authenticated with
 * `INTERNAL_WORKER_SECRET` — see `requireInternalWorker`
 * (`apps/api/src/middlewares/require-internal-worker.ts`) for why that's a
 * separate mechanism from a user session.
 */

/** One reference technique, as returned by the public `GET /reference/tech-steps` — this worker's only source of the taxonomy it audits/labels against, never a hardcoded copy (see `tech-step-taxonomy.ts`). */
export interface TechStepReference {
  id: number;
  key: string;
}

/** Mirrors `TechStepAuditClauseView` (`packages/shared`) — duplicated here rather than importing from `@batch-cooking/shared`, since this worker deliberately lives outside the pnpm workspace (see `package.json`'s own doc comment) and so can't depend on a workspace package. */
export interface AuditClause {
  stepId: number;
  recipeId: number;
  clauseText: string;
  anchorKey: string | null;
  intentKey: string | null;
  score: number;
  locale: string;
}

/** Mirrors `PendingTechStepCorrectionView` (`packages/shared`) — same "duplicated, not imported" reasoning as {@link AuditClause}. */
export interface PendingCorrection {
  id: number;
  stepId: number;
  recipeId: number;
  clauseText: string;
  start: number;
  end: number;
  previousTechStepKey: string | null;
  correctedTechStepKey: string | null;
}

/** One suggestion `postTrainingSuggestions` submits — mirrors one entry of `SubmitTrainingSuggestionsInput["suggestions"]` (`packages/shared`). */
export interface TrainingSuggestionInput {
  techStepKey: string;
  locale: string;
  suggestedSynonyms: string[];
  suggestedUtterances: string[];
  sourceType: "correction" | "llm_audit";
  sourceCorrectionId?: number | null;
}

async function request<TResponseBody>(
  path: string,
  init: RequestInit = {},
): Promise<TResponseBody> {
  try {
    const response = await fetch(`${env.API_BASE_URL}${path}`, {
      ...init,
      headers: {
        "Content-Type": "application/json",
        "X-Internal-Worker-Secret": env.INTERNAL_WORKER_SECRET,
        ...init.headers,
      },
    });
    if (!response.ok) {
      const body = await response.text().catch(() => "");
      throw new Error(`${init.method ?? "GET"} ${path} failed: ${response.status} ${body}`);
    }
    return (await response.json()) as TResponseBody;
  } catch (err) {
    // Rethrown as-is — every caller (the scheduler's per-job try/catch,
    // see `scheduler.ts`) already decides what to do with a failed run;
    // this is just the one place the `await` itself has to sit inside a
    // try/catch, same convention `apps/api` follows for the same reason.
    throw err;
  }
}

/** The full reference technique catalog — `apps/api`'s `TechStep` table, read fresh (never cached beyond one process's lifetime) so a catalog change is picked up on the worker's next restart without a code change here. */
export function getTechStepReference(): Promise<TechStepReference[]> {
  return request("/reference/tech-steps");
}

/** Low-confidence clauses sampled from existing recipes — `audit-low-confidence`'s raw material. */
export function getAuditBatch(locale: string, limit: number): Promise<AuditClause[]> {
  const params = new URLSearchParams({ locale, limit: String(limit) });
  return request(`/internal/tech-steps/audit-batch?${params}`);
}

/** Corrections not yet turned into a suggestion — `transform-corrections`'s raw material. */
export function getPendingCorrections(limit: number): Promise<PendingCorrection[]> {
  const params = new URLSearchParams({ limit: String(limit) });
  return request(`/internal/tech-steps/pending-corrections?${params}`);
}

/** Payload of `POST /internal/tech-steps/heartbeat` — mirrors `workerHeartbeatSchema` (`packages/shared`), duplicated here for the same "outside the workspace" reason as {@link AuditClause}. */
export interface HeartbeatPayload {
  event: "boot" | "tick" | "job";
  job?: string;
  ok?: boolean;
  counts?: Record<string, number>;
}

/**
 * Best-effort liveness ping to `apps/api` so the admin monitoring board can
 * see this worker (which has no inbound HTTP surface of its own). Sent on
 * boot, on every scheduler tick, and after each job. **Never throws** — a
 * failed heartbeat must never break or abort a run; it's logged and
 * swallowed here.
 */
export async function postHeartbeat(payload: HeartbeatPayload): Promise<void> {
  try {
    await request("/internal/tech-steps/heartbeat", {
      method: "POST",
      body: JSON.stringify(payload),
    });
  } catch (err) {
    console.warn(
      "[tech-step-llm-worker] heartbeat failed:",
      err instanceof Error ? err.message : err,
    );
  }
}

/** Submits a batch of suggestions — a no-op (resolves immediately) if `suggestions` is empty, so a job with nothing to report doesn't need its own guard at every call site. */
export function postTrainingSuggestions(
  suggestions: TrainingSuggestionInput[],
): Promise<{ created: number }> {
  if (suggestions.length === 0) return Promise.resolve({ created: 0 });
  return request("/internal/tech-steps/training-suggestions", {
    method: "POST",
    body: JSON.stringify({ suggestions }),
  });
}
