import type {
  ParsedRecipe,
  RecipeSourceAdapter,
  RecipeSourceListParams,
  RecipeSourceListResult,
} from "../lib/recipe-sources/recipe-source-adapter.js";
import {
  RecipeSourceFetchError,
  RecipeSourceParseError,
} from "../lib/recipe-sources/recipe-source-errors.js";

const SOURCE_KEY = "theMealDb";

// TheMealDB documents "1" as a shared, public test key, free to use for
// development (https://www.themealdb.com/api.php) — a deployment serving
// real traffic is expected to use a supporter-tier key instead (paid, via
// Patreon). Configurable here via an env var without touching anything
// else in this adapter.
const API_KEY = process.env.THE_MEAL_DB_API_KEY ?? "1";
const API_BASE = `https://www.themealdb.com/api/json/v1/${API_KEY}`;

/**
 * TheMealDB's flat meal shape — ingredients/measures are 20 numbered
 * field pairs (`strIngredient1`/`strMeasure1` … `strIngredient20`/
 * `strMeasure20`), not an array, hence the string index signature rather
 * than 20 explicit optional properties.
 */
export interface TheMealDbMeal {
  idMeal: string;
  strMeal: string | null;
  strMealThumb: string | null;
  strInstructions: string | null;
  [key: string]: string | null | undefined;
}

interface TheMealDbMealsResponse {
  meals: TheMealDbMeal[] | null;
}

async function fetchTheMealDb<T>(path: string): Promise<T> {
  try {
    let response: Response;
    try {
      response = await fetch(`${API_BASE}${path}`);
    } catch (cause) {
      throw new RecipeSourceFetchError(SOURCE_KEY, `Network error calling TheMealDB (${path})`, {
        cause,
      });
    }
    if (!response.ok) {
      throw new RecipeSourceFetchError(
        SOURCE_KEY,
        `TheMealDB responded ${response.status} (${path})`,
      );
    }
    return (await response.json()) as T;
  } catch (err) {
    // Rethrown as-is — this adapter's only caller (`sources.service.ts`)
    // already handles/logs failures centrally; this method just isn't
    // allowed a bare `await` per the repo's async/try-catch convention.
    throw err;
  }
}

function detailUrl(idMeal: string): string {
  return `https://www.themealdb.com/meal/${idMeal}`;
}

/**
 * TheMealDB (themealdb.com) — a free, public recipe API (no scraping: the
 * publisher's own structured JSON, hence `official: true`). The first real
 * `RecipeSourceAdapter` implementation, proving the generic contract
 * (recipe-source-adapter.ts) end to end against a live source.
 *
 * `list()` is search-only — TheMealDB has no dedicated "browse everything"
 * endpoint on its free tier. An omitted `query` searches for an empty
 * string, which TheMealDB happens to answer with a small default sample
 * (~25 meals) rather than nothing — close enough to this contract's
 * "omitted `query` means browse everything" convention
 * (`RecipeSourceListParams.query`) to lean on as-is, though it's a fixed
 * sample, not the whole catalog. Search isn't paginated either — one
 * response holds every match, so `nextCursor` is always `null`.
 */
export const theMealDbAdapter: RecipeSourceAdapter<TheMealDbMeal> = {
  key: SOURCE_KEY,
  name: "TheMealDB",
  official: true,
  iconUrl: "https://www.themealdb.com/images/logo.svg",
  // TheMealDB's content (names, ingredients, instructions) is English —
  // determines which locale translateRecipe (recipe-translation.ts)
  // resolves this source's recipes against when previewing/importing one.
  locale: "en",

  async list(params: RecipeSourceListParams): Promise<RecipeSourceListResult> {
    try {
      const query = params.query ?? "";
      const data = await fetchTheMealDb<TheMealDbMealsResponse>(
        `/search.php?s=${encodeURIComponent(query)}`,
      );
      const meals = data.meals ?? [];
      return {
        items: meals
          .filter((meal): meal is TheMealDbMeal & { strMeal: string } => Boolean(meal.strMeal))
          .map((meal) => ({
            externalId: meal.idMeal,
            title: meal.strMeal,
            picture: meal.strMealThumb,
            url: detailUrl(meal.idMeal),
          })),
        nextCursor: null,
      };
    } catch (err) {
      throw err; // see fetchTheMealDb()'s catch comment above
    }
  },

  async fetchDetail(externalId: string): Promise<TheMealDbMeal> {
    try {
      const data = await fetchTheMealDb<TheMealDbMealsResponse>(`/lookup.php?i=${externalId}`);
      const meal = data.meals?.[0];
      if (!meal) {
        throw new RecipeSourceFetchError(SOURCE_KEY, `No meal found for id "${externalId}"`);
      }
      return meal;
    } catch (err) {
      throw err; // see fetchTheMealDb()'s catch comment above
    }
  },

  parse(meal: TheMealDbMeal): ParsedRecipe {
    if (!meal.strMeal) {
      throw new RecipeSourceParseError(SOURCE_KEY, "Meal is missing its name (strMeal)");
    }

    const ingredients = [];
    for (let i = 1; i <= 20; i++) {
      const name = meal[`strIngredient${i}`]?.trim();
      if (!name) continue;
      const measure = meal[`strMeasure${i}`]?.trim();
      ingredients.push({
        rawText: measure ? `${measure} ${name}` : name,
        quantity: null,
        unit: null,
        name,
      });
    }

    // Free-text instructions, usually one step per line — splitting on
    // blank/newlines is the closest this source gets to discrete steps.
    // TheMealDB frequently numbers each step on its own line ahead of the
    // paragraph that follows (e.g. "…melted.\n\n2\n\nPreheat oven…") rather
    // than inline ("1. Preheat oven…") — the blank-line split above turns
    // that lone number into its own "line", which would otherwise become a
    // bogus step containing nothing but a digit. Drop those rather than
    // keep them as steps in their own right (see issue #52).
    const steps = (meal.strInstructions ?? "")
      .split(/\r?\n+/)
      .map((line) => line.trim())
      .filter((line) => line.length > 0)
      .filter((line) => !/^\d+\.?$/.test(line))
      .map((description) => ({ description, picture: null }));
    if (steps.length === 0) {
      throw new RecipeSourceParseError(
        SOURCE_KEY,
        `Meal "${meal.strMeal}" has no usable instructions`,
      );
    }

    return {
      name: meal.strMeal,
      description: null,
      picture: meal.strMealThumb,
      // TheMealDB's free API doesn't state a serving size.
      portions: null,
      sourceUrl: detailUrl(meal.idMeal),
      ingredients,
      steps,
    };
  },
};
