import type { RecipesRepository } from '../recipes/recipes.repository.js';
import type { PantryRepository } from '../pantry/pantry.repository.js';
import type { MealPlanRepository } from './meal-plans.repository.js';
import type { NutritionTargetRepository } from '../nutrition-targets/nutrition-target.repository.js';

interface Deps {
  recipesRepository: RecipesRepository;
  pantryRepository: PantryRepository;
  mealPlanRepository: MealPlanRepository;
  nutritionTargetRepository: NutritionTargetRepository;
}

export interface ScoredRecipe {
  recipeId: string;
  recipeName: string;
  totalScore: number;
  scores: {
    coverage: number;
    urgency: number;
    nutrition: number;
    variety: number;
  };
  reasoning: string[];
}

export class SuggestionEngineService {
  private readonly recipesRepository: RecipesRepository;
  private readonly pantryRepository: PantryRepository;
  private readonly mealPlanRepository: MealPlanRepository;
  private readonly nutritionTargetRepository: NutritionTargetRepository;

  public constructor({
    recipesRepository,
    pantryRepository,
    mealPlanRepository,
    nutritionTargetRepository,
  }: Deps) {
    this.recipesRepository = recipesRepository;
    this.pantryRepository = pantryRepository;
    this.mealPlanRepository = mealPlanRepository;
    this.nutritionTargetRepository = nutritionTargetRepository;
  }

  public async getSuggestions(
    householdId: string,
    userId: string,
    options: { limit?: number } = {},
  ): Promise<ScoredRecipe[]> {
    const limit = options.limit ?? 5;

    // 1. Parallel fetch state
    const [recipesResult, pantryItems, activeTarget, recentPlansResult] = await Promise.all([
      this.recipesRepository.findByHousehold(householdId, { limit: 1000 }),
      this.pantryRepository.findActiveByHousehold(householdId),
      this.nutritionTargetRepository.findByUser(userId, householdId),
      this.mealPlanRepository.findByHousehold(householdId, { limit: 5 }), // scan last ~5 weeks to cover 14 days
    ]);

    const recipes = recipesResult.data;

    // 2. Process Pantry inventory for fast O(1) access
    // Map of productId -> { totalQty, minDaysRemaining, maxUrgencyWeight }
    const pantryInventory = new Map<
      string,
      { qty: number; minDays: number; maxUrgencyWeight: number }
    >();

    for (const item of pantryItems) {
      const rec = item as Record<string, unknown>;
      const productId = rec.productId as string;
      const quantity = (rec.quantity as number) ?? 0;
      const fresh = (rec.freshnessEstimate ?? {}) as Record<string, unknown>;
      const daysRemaining = (fresh.daysRemaining as number) ?? 999;
      const urgencyStr = (fresh.urgency as string) || 'normal';

      const urgencyWeight = this.getUrgencyWeight(urgencyStr);

      const existing = pantryInventory.get(productId);
      if (existing) {
        existing.qty += quantity;
        existing.minDays = Math.min(existing.minDays, daysRemaining);
        existing.maxUrgencyWeight = Math.max(existing.maxUrgencyWeight, urgencyWeight);
      } else {
        pantryInventory.set(productId, {
          qty: quantity,
          minDays: daysRemaining,
          maxUrgencyWeight: urgencyWeight,
        });
      }
    }

    // 3. Build History Map for Variety scoring (last 14 days)
    const lastEaten = new Map<string, number>(); // recipeId -> daysAgo
    const now = new Date();

    for (const plan of recentPlansResult.data) {
      const recPlan = plan as Record<string, unknown>;
      const days = (recPlan.days as any[]) || [];
      for (const day of days) {
        const dateStr = day.date as string;
        const dayDate = new Date(dateStr);
        const diffTime = Math.abs(now.getTime() - dayDate.getTime());
        const daysAgo = Math.ceil(diffTime / (1000 * 60 * 60 * 24));

        if (daysAgo <= 14) {
          const meals = (day.meals as any[]) || [];
          for (const meal of meals) {
            const recipeId = meal.recipeId as string;
            if (!recipeId) continue;

            const currentMin = lastEaten.get(recipeId);
            if (currentMin === undefined || daysAgo < currentMin) {
              lastEaten.set(recipeId, daysAgo);
            }
          }
        }
      }
    }

    // 4. Score each recipe
    const scoredList: ScoredRecipe[] = [];

    for (const recipe of recipes) {
      const rec = recipe as Record<string, unknown>;
      const recipeId = (rec._id as { toString(): string }).toString();
      const ingredients = (rec.ingredients as any[]) || [];
      const recipeName = (rec.name as string) || 'Unknown Recipe';

      const scoreBreakdown = this.scoreRecipe(
        ingredients,
        rec.perServingNutrition as Record<string, number> | undefined,
        recipeId,
        pantryInventory,
        activeTarget as Record<string, unknown> | null,
        lastEaten,
      );

      const totalScore =
        scoreBreakdown.coverage * 0.4 +
        scoreBreakdown.urgency * 0.3 +
        scoreBreakdown.nutrition * 0.2 +
        scoreBreakdown.variety * 0.1;

      const roundedScore = Math.round(totalScore * 1000) / 1000;

      scoredList.push({
        recipeId,
        recipeName,
        totalScore: roundedScore,
        scores: scoreBreakdown,
        reasoning: this.generateReasoning(scoreBreakdown, recipeName),
      });
    }

    // 5. Sort descending and limit
    return scoredList.sort((a, b) => b.totalScore - a.totalScore).slice(0, limit);
  }

  private scoreRecipe(
    ingredients: any[],
    nutrition: Record<string, number> | undefined,
    recipeId: string,
    inventory: Map<string, { qty: number; minDays: number; maxUrgencyWeight: number }>,
    target: Record<string, unknown> | null,
    lastEaten: Map<string, number>,
  ) {
    // Filter non-optional ingredients
    const requiredIngs = ingredients.filter((ing) => !ing.isOptional);

    // -- COVERAGE & URGENCY --
    let coverageScore = 1.0;
    let urgencyScore = 0.0;

    if (requiredIngs.length > 0) {
      let totalMatches = 0;
      let weightedUrgencySum = 0;

      for (const ing of requiredIngs) {
        const inv = inventory.get(ing.productId);
        if (inv && inv.qty > 0) {
          // If we have full quantity, 1.0, else partial
          const coverageFraction = Math.min(1.0, inv.qty / ing.quantity);
          totalMatches += coverageFraction;
          weightedUrgencySum += inv.maxUrgencyWeight * coverageFraction;
        }
      }

      coverageScore = totalMatches / requiredIngs.length;
      // Normalize urgency as average urgency over ingredients used
      urgencyScore = totalMatches > 0 ? weightedUrgencySum / totalMatches : 0.0;
    }

    // -- NUTRITION --
    let nutritionScore = 1.0; // Neutral by default
    if (target && nutrition && nutrition.calories > 0) {
      const tCals = (target.dailyCalories as number) || 2000;
      const tP = (target.proteinG as number) || 0;
      const tC = (target.carbsG as number) || 0;
      const tF = (target.fatG as number) || 0;

      const rCals = nutrition.calories;
      const rP = nutrition.protein || 0;
      const rC = nutrition.carbs || 0;
      const rF = nutrition.fat || 0;

      if (tP || tC || tF) {
        // Target macro percentages
        const targetProtPct = (tP * 4) / tCals;
        const targetCarbPct = (tC * 4) / tCals;
        const targetFatPct = (tF * 9) / tCals;

        // Recipe macro percentages
        const recProtPct = (rP * 4) / rCals;
        const recCarbPct = (rC * 4) / rCals;
        const recFatPct = (rF * 9) / rCals;

        // Sum of absolute diffs
        const diff =
          Math.abs(targetProtPct - recProtPct) +
          Math.abs(targetCarbPct - recCarbPct) +
          Math.abs(targetFatPct - recFatPct);

        // Lower diff means higher score. Max diff is 2.0 theoretically.
        nutritionScore = Math.max(0, 1.0 - diff / 2.0);
      }
    }

    // -- VARIETY --
    let varietyScore = 1.0;
    const daysAgo = lastEaten.get(recipeId);
    if (daysAgo !== undefined) {
      // Linear scaling over 14 days
      varietyScore = daysAgo / 14;
    }

    return {
      coverage: Math.round(coverageScore * 100) / 100,
      urgency: Math.round(urgencyScore * 100) / 100,
      nutrition: Math.round(nutritionScore * 100) / 100,
      variety: Math.round(varietyScore * 100) / 100,
    };
  }

  private getUrgencyWeight(urgency: string): number {
    switch (urgency) {
      case 'expired':
      case 'urgent':
        return 1.0;
      case 'soon':
      case 'expiringSoon':
        return 0.7;
      case 'normal':
        return 0.3;
      case 'fresh':
        return 0.1;
      default:
        return 0.0;
    }
  }

  private generateReasoning(
    scores: { coverage: number; urgency: number; nutrition: number; variety: number },
    recipeName: string,
  ): string[] {
    const reasons: string[] = [];

    if (scores.coverage > 0.9) {
      reasons.push(`You have almost all the ingredients for ${recipeName} right now.`);
    } else if (scores.coverage > 0.5) {
      reasons.push(`Uses several ingredients already stocked in your pantry.`);
    }

    if (scores.urgency > 0.7) {
      reasons.push(`High priority: Saves expiring pantry items from going to waste!`);
    } else if (scores.urgency > 0.4) {
      reasons.push(`Helps use up items that should be consumed soon.`);
    }

    if (scores.nutrition > 0.85) {
      reasons.push(`Matches your daily nutritional macro goals very closely.`);
    }

    if (scores.variety > 0.95 && (scores.coverage > 0.5 || scores.urgency > 0.5)) {
      reasons.push(`You haven't had this in over two weeks, adding good variety.`);
    }

    return reasons;
  }
}
