/**
 * The generic contract every recipe source (a specific website, an API, …)
 * implements — groundwork for the "Import d'une recette" pipeline described
 * in specs/batch-cooking-architecture.md (import depuis source → traduction
 * en étapes → sauvegarde). This file only defines the shapes; no concrete
 * source exists yet (see `recipe-source-registry.ts` for where one would be
 * registered) and nothing here talks to the database or an HTTP route —
 * that wiring (persisting an imported recipe, resolving `sourceId`) is
 * deliberately out of scope until a real source needs it.
 *
 * The flow a caller drives against one adapter:
 *   1. `list()` — browse what's available from the source (paginated,
 *      optionally filtered by `query`), like flipping through a catalog.
 *   2. {@link markAlreadyImported} — flag which of those items we've
 *      already imported, so browsing a source doesn't dangle recipes the
 *      user has already brought in as if they were new. A separate, pure
 *      step rather than something `list()` itself does: an adapter only
 *      knows its source, never our database — same reasoning as
 *      `tech-step-matcher.ts`'s split between pure `splitIntoClauses` and
 *      its DB/model-touching `TechStepClassifierService`. Whichever future
 *      layer queries "which externalIds from this source do we already have"
 *      (not yet decided — it needs a place to persist that link,
 *      see {@link RecipeSourceListItem.externalId}) calls this to annotate
 *      the page before returning it.
 *   3. `fetchDetail(externalId)` — once the user picks one item from that
 *      list, fetch its full raw content.
 *   4. `parse(raw)` — turn that raw content into a {@link ParsedRecipe},
 *      pure and synchronous so it's unit-testable without any network
 *      access (same split as step 2 above).
 */

/** Search/pagination input for {@link RecipeSourceAdapter.list}. */
export interface RecipeSourceListParams {
  /** Free-text search, if the source supports it. Omitted means "browse everything". */
  query?: string;
  /**
   * Opaque continuation token from a previous {@link RecipeSourceListResult.nextCursor}
   * — omitted (or `null`) means "start from the first page". Deliberately
   * opaque (not a page number) so an adapter can back it with whatever its
   * source actually supports (page number, offset, an API-provided token).
   */
  cursor?: string | null;
}

/** One entry in a {@link RecipeSourceAdapter.list} result — enough to show in a browsing UI and to fetch the full recipe once selected. */
export interface RecipeSourceListItem {
  /**
   * Source-specific identifier, opaque to callers — passed back verbatim
   * to {@link RecipeSourceAdapter.fetchDetail}, and the key
   * {@link markAlreadyImported} matches against to tell an already-imported
   * item apart from a new one.
   */
  externalId: string;
  title: string;
  picture: string | null;
  /** Canonical URL of the recipe on the source, kept for attribution even before it's imported. */
  url: string;
}

export interface RecipeSourceListResult {
  items: RecipeSourceListItem[];
  /** Pass back as `cursor` to fetch the next page — `null` means this was the last page. */
  nextCursor: string | null;
}

/** A browsed {@link RecipeSourceListItem}, after {@link markAlreadyImported} has flagged whether we already imported it. What a browsing UI actually renders — e.g. to grey it out or offer "already added" instead of "import". */
export interface BrowsableRecipeItem extends RecipeSourceListItem {
  alreadyImported: boolean;
}

/**
 * Splits a page of {@link RecipeSourceListItem}s into already-imported vs.
 * new, purely by checking each item's `externalId` against
 * `importedExternalIds` — no I/O here, the caller is responsible for
 * gathering that set (from wherever we end up persisting the link between
 * an imported `Recipe` and the source item it came from) before calling
 * this. Kept as a tiny, dedicated, easily-testable step rather than folded
 * into `list()` itself, so an adapter never needs to know our database
 * exists.
 */
export function markAlreadyImported(
  items: RecipeSourceListItem[],
  importedExternalIds: ReadonlySet<string>,
): BrowsableRecipeItem[] {
  return items.map((item) => ({
    ...item,
    alreadyImported: importedExternalIds.has(item.externalId),
  }));
}

/**
 * One ingredient line as lifted from a source, before it's resolved against
 * our own `Ingredient`/`Unit` reference catalogs (that resolution —
 * matching free text to a `key`, the way `tech-step-matcher.ts` matches
 * step text to a `TechStep` — is a separate, not-yet-built concern; this
 * type only carries what a source's raw text actually says). `rawText` is
 * kept alongside the (best-effort) parsed fields so a failed/partial parse
 * is still traceable back to what the source originally wrote.
 */
export interface ParsedRecipeIngredient {
  rawText: string;
  quantity: number | null;
  /** Free-text unit exactly as written by the source (e.g. `"cuillère à soupe"`, `"g"`) — not yet resolved to a `Unit.key`. */
  unit: string | null;
  /** Free-text ingredient name exactly as written by the source — not yet resolved to an `Ingredient.key`. */
  name: string;
}

export interface ParsedRecipeStep {
  description: string;
  picture: string | null;
}

/**
 * The normalized shape every adapter's {@link RecipeSourceAdapter.parse}
 * produces, regardless of the source. Intentionally *not*
 * `CreateRecipeInput` (packages/shared/src/schemas/recipe.ts): ingredients
 * are still free text (no `ingredientId`/`unitId` — that catalog-matching
 * step doesn't exist yet), and there's no `dietIds`/`visibility` since a
 * source can't know those. Turning a `ParsedRecipe` into a saved `Recipe`
 * is future work for whichever module ends up driving this pipeline.
 */
export interface ParsedRecipe {
  name: string;
  description: string | null;
  picture: string | null;
  /** `null` when the source doesn't state a serving size. */
  portions: number | null;
  /** Canonical URL of the recipe on the source — the eventual `Source`/`Recipe.sourceId` link (schema.prisma) is populated from this once the import pipeline saves the recipe. */
  sourceUrl: string;
  ingredients: ParsedRecipeIngredient[];
  steps: ParsedRecipeStep[];
}

/**
 * A single recipe source — a specific website or API, plus the two pieces
 * of source-specific logic needed to pull a recipe out of it. `TRawDetail`
 * is whatever shape `fetchDetail` naturally returns for this source (an
 * HTML string, a parsed JSON body, …); `parse` is the only thing that needs
 * to understand it.
 *
 * @example
 * ```ts
 * const myAdapter: RecipeSourceAdapter<{ html: string }> = {
 *   key: "someRecipeSite",
 *   name: "Some Recipe Site",
 *   official: false,
 *   iconUrl: "https://somerecipesite.example/favicon.svg",
 *   locale: "fr",
 *   async list(params) { ... },
 *   async fetchDetail(externalId) { ... },
 *   parse(raw) { ... },
 * };
 * registerRecipeSource(myAdapter);
 * ```
 */
export interface RecipeSourceAdapter<TRawDetail = unknown> {
  /** Stable identifier used to look this adapter up in the registry — same "English camelCase uid" convention as `Diet.key`/`Unit.key`/`TechStep.key`. */
  key: string;
  /** Human-readable name, for display in a source picker. */
  name: string;
  /**
   * Whether this source is an official API (the site/publisher itself
   * provides structured recipe data) versus unofficial web scraping (we
   * parse HTML the site never committed to a stable shape for) — surfaced
   * to households (`Source.official`, synced via `syncRecipeSources`) so
   * they can tell the two apart when deciding which sources to enable
   * (see `HouseSource`, schema.prisma). No default on purpose: every
   * adapter author has to consciously pick one rather than silently
   * inheriting a guess.
   */
  official: boolean;
  /** URL of the source's own logo/favicon, for `SourceSelect` (apps/web) to display next to its name — `null` if the source has none worth showing. Synced to `Source.iconUrl` the same way as `name`/`official`. */
  iconUrl: string | null;
  /**
   * Language of the text this source produces (`ParsedRecipe.description`/
   * `steps[].description`/`ingredients[].name`) — e.g. `"en"` for
   * TheMealDB. Not a user preference: the language the source's own
   * content is actually written in, regardless of who's browsing it.
   * Determines which trained-classifier/ingredient-label locale
   * `translateRecipe` (`recipe-translation.ts`) resolves this source's
   * recipes against when previewing/importing one.
   */
  locale: string;
  list(params: RecipeSourceListParams): Promise<RecipeSourceListResult>;
  fetchDetail(externalId: string): Promise<TRawDetail>;
  parse(raw: TRawDetail): ParsedRecipe;
}
