import type { ProductsRepository } from '../products/products.repository.js';
import type { RecipesRepository } from './recipes.repository.js';
import { NutritionCalculatorService } from './nutrition-calculator.service.js';
import { toMetric } from './unit-conversion.service.js';
import { NotFoundError, BadRequestError } from '../../common/errors.js';
import type {
  CreateRecipeInput,
  UpdateRecipeInput,
  RecipeQueryInput,
  ScaleRecipeInput,
  RecipeIngredient,
  NutritionInfo,
} from '@meshitrack/shared';
import type { ILlmProvider } from '../llm/llm-provider.interface.js';

interface Deps {
  recipesRepository: RecipesRepository;
  productsRepository: ProductsRepository;
  llmProvider: ILlmProvider;
}

interface ConversionError {
  ingredientIndex: number;
  productId: string;
  code: string;
  message: string;
}

export class RecipesService {
  private readonly recipesRepository: RecipesRepository;
  private readonly productsRepository: ProductsRepository;
  private readonly llmProvider: ILlmProvider;
  private readonly nutritionCalculator: NutritionCalculatorService;

  public constructor({ recipesRepository, productsRepository, llmProvider }: Deps) {
    this.recipesRepository = recipesRepository;
    this.productsRepository = productsRepository;
    this.llmProvider = llmProvider;
    this.nutritionCalculator = new NutritionCalculatorService();
  }

  public async list(householdId: string, query: RecipeQueryInput) {
    return this.recipesRepository.findByHousehold(householdId, query);
  }

  public async getById(id: string, householdId: string) {
    const recipe = await this.recipesRepository.findById(id, householdId);
    if (!recipe) throw new NotFoundError('Recipe not found');
    return recipe;
  }

  public async create(data: CreateRecipeInput, householdId: string, createdBy: string) {
    const { normalizedIngredients, productMap } = await this.resolveAndNormalizeIngredients(
      data.ingredients,
      householdId,
    );

    const { totalNutrition, perServingNutrition, ingredientContributions } =
      this.nutritionCalculator.calculateRecipeNutrition(
        normalizedIngredients,
        productMap,
        data.servings,
      );

    const warnings = this.nutritionCalculator.generateWarnings(perServingNutrition);

    const storedIngredients = normalizedIngredients.map((ni, i) => ({
      ...ni,
      nutritionContribution: ingredientContributions[i]!,
    }));

    const { ingredients: _ingredients, ...rest } = data;
    return this.recipesRepository.create(
      rest,
      {
        ingredients: storedIngredients,
        totalNutrition,
        perServingNutrition,
        warnings,
      },
      householdId,
      createdBy,
    );
  }

  public async update(id: string, householdId: string, data: UpdateRecipeInput) {
    await this.getById(id, householdId);

    let computed:
      | Partial<{
          ingredients: RecipeIngredient[];
          totalNutrition: NutritionInfo;
          perServingNutrition: NutritionInfo;
          warnings: string[];
        }>
      | undefined;

    if (data.ingredients || data.servings !== undefined) {
      // Re-fetch current recipe to get current servings if not updating them
      const current = await this.recipesRepository.findById(id, householdId);
      const servings = data.servings ?? current!.servings;
      const ingredientsInput =
        data.ingredients ??
        current!.ingredients.map((i) => ({
          productId: i.productId,
          productName: i.productName,
          quantity: i.quantity,
          unit: i.unit as string,
          ...(i.preparation ? { preparation: i.preparation } : {}),
          isOptional: i.isOptional,
        }));

      const { normalizedIngredients, productMap } = await this.resolveAndNormalizeIngredients(
        ingredientsInput,
        householdId,
      );

      const { totalNutrition, perServingNutrition, ingredientContributions } =
        this.nutritionCalculator.calculateRecipeNutrition(
          normalizedIngredients,
          productMap,
          servings,
        );

      const warnings = this.nutritionCalculator.generateWarnings(perServingNutrition);

      computed = {
        ingredients: normalizedIngredients.map((ni, i) => ({
          ...ni,
          nutritionContribution: ingredientContributions[i]!,
        })),
        totalNutrition,
        perServingNutrition,
        warnings,
      };
    }

    const { ingredients: _ingredients, ...rest } = data;
    const updated = await this.recipesRepository.update(id, householdId, rest, computed);
    if (!updated) throw new NotFoundError('Recipe not found');
    return updated;
  }

  public async delete(id: string, householdId: string) {
    await this.getById(id, householdId);
    const deleted = await this.recipesRepository.softDelete(id, householdId);
    if (!deleted) throw new NotFoundError('Recipe not found');
    return deleted;
  }

  public async scale(id: string, householdId: string, input: ScaleRecipeInput) {
    const recipe = await this.getById(id, householdId);
    const factor = input.targetServings / recipe.servings;

    const scaledIngredients = recipe.ingredients.map((ing) => ({
      ...ing,
      quantity: Math.round(ing.quantity * factor * 1000) / 1000,
    }));

    const productMap = await this.buildProductMap(
      householdId,
      recipe.ingredients.map((i) => i.productId),
    );

    const { totalNutrition, perServingNutrition, ingredientContributions } =
      this.nutritionCalculator.calculateRecipeNutrition(
        scaledIngredients,
        productMap,
        input.targetServings,
      );

    return {
      ...recipe,
      servings: input.targetServings,
      ingredients: scaledIngredients.map((ing, i) => ({
        ...ing,
        nutritionContribution: ingredientContributions[i]!,
      })),
      totalNutrition,
      perServingNutrition,
    };
  }

  public async importFromText(text: string, _householdId: string) {
    const parsed = await this.llmProvider.parseRecipe(text);
    if (!parsed) return { available: false as const };
    return { available: true as const, draft: parsed };
  }

  public async importFromUrl(url: string, _householdId: string) {
    const parsed = await this.llmProvider.parseRecipeFromUrl(url);
    if (!parsed) return { available: false as const };
    return { available: true as const, draft: parsed };
  }

  public async findByProduct(
    householdId: string,
    productId: string,
    query: { cursor?: string; limit?: number },
  ) {
    return this.recipesRepository.findByProductId(householdId, productId, query);
  }

  /** Called by products service after a product nutrition update. */
  public async recalculateForProduct(householdId: string, productId: string) {
    const recipes = await this.recipesRepository.findAllByProductId(householdId, productId);

    for (const recipe of recipes) {
      const productIds = recipe.ingredients.map((i) => i.productId);
      const productMap = await this.buildProductMap(householdId, productIds);

      const { totalNutrition, perServingNutrition, ingredientContributions } =
        this.nutritionCalculator.calculateRecipeNutrition(
          recipe.ingredients,
          productMap,
          recipe.servings,
        );

      const warnings = this.nutritionCalculator.generateWarnings(perServingNutrition);

      const updatedIngredients = recipe.ingredients.map((ing, i) => ({
        ...ing,
        nutritionContribution: ingredientContributions[i]!,
      }));

      await this.recipesRepository.update(
        recipe._id.toString(),
        householdId,
        {},
        {
          ingredients: updatedIngredients as RecipeIngredient[],
          totalNutrition,
          perServingNutrition,
          warnings,
        },
      );
    }
  }

  private async resolveAndNormalizeIngredients(
    ingredients: Array<{
      productId: string;
      productName: string;
      quantity: number;
      unit: string;
      preparation?: string;
      isOptional?: boolean;
    }>,
    householdId: string,
  ) {
    const productIds = [...new Set(ingredients.map((i) => i.productId))];
    const products = await this.productsRepository.findByIds(householdId, productIds);
    const productMap = new Map(products.map((p) => [p._id.toString(), p]));

    const conversionErrors: ConversionError[] = [];
    const normalizedIngredients: Array<Omit<RecipeIngredient, 'nutritionContribution'>> = [];

    for (let i = 0; i < ingredients.length; i++) {
      const ing = ingredients[i]!;
      const product = productMap.get(ing.productId);

      if (!product) {
        throw new NotFoundError(`Product "${ing.productId}" not found in household`);
      }

      const result = toMetric(
        ing.quantity,
        ing.unit as never,
        product.servingUnit as 'g' | 'ml' | 'piece' | 'slice',
        product.densityGPerMl ?? undefined,
      );

      if (!result.ok) {
        conversionErrors.push({
          ingredientIndex: i,
          productId: ing.productId,
          code: result.code,
          message: result.message,
        });
        continue;
      }

      normalizedIngredients.push({
        productId: ing.productId,
        productName: ing.productName,
        quantity: result.quantity,
        unit: result.unit,
        ...(ing.unit !== result.unit
          ? { originalQuantity: ing.quantity, originalUnit: ing.unit as never }
          : {}),
        ...(ing.preparation ? { preparation: ing.preparation } : {}),
        isOptional: ing.isOptional ?? false,
      });
    }

    if (conversionErrors.length > 0) {
      throw new BadRequestError('Unit conversion failed for one or more ingredients', {
        conversionErrors: conversionErrors.map((e) => e.message),
      });
    }

    return { normalizedIngredients, productMap };
  }

  private async buildProductMap(householdId: string, productIds: string[]) {
    const products = await this.productsRepository.findByIds(householdId, productIds);
    return new Map(products.map((p) => [p._id.toString(), p]));
  }
}
