import { and, desc, eq, gt, inArray, isNull, or, sql } from "drizzle-orm";
import { schema, type Database } from "@app/database";
import type { MemoryItem, TasteSignal } from "@app/shared-types";
import {
  computeCoverage,
  checkRecipeSafety,
  isRecipeSafe,
  type IngredientSafetyInfo,
  type PantryItem,
} from "@app/recipe-engine";
import {
  isEventActive,
  parseCraving,
  rankAll,
  seasonForDate,
  viewWeights,
  type CookingAssumption,
  type RecommendationCandidate,
  type RecommendationContext,
  type RecommendationView,
  type ScoredRecommendation,
} from "@app/recommendation-engine";
import { DEFAULT_TARGETS, computeDailyTargets, summarizeDay } from "@app/nutrition-engine";
import { getActiveHouseholdId, todayIso } from "./helpers.js";

export interface MemoryImpactOptions {
  db: Database;
  userId: string;
  memoryItem: MemoryItem;
  mealType?: string;
  persons?: number;
  maxMinutes?: number;
  craving?: string;
  view?: "default" | "taste" | "health" | "pantry";
  limit?: number;
}

export interface MemoryImpactResult {
  memoryItemId: string;
  personalizationEnabled: boolean;
  impacted: Array<{
    recipeId: string;
    titleSv: string;
    scoreWith: number;
    scoreWithout: number;
    delta: number;
    whySv: string;
    provenance: Array<{ key: string; args: Record<string, string | number> }>;
  }>;
}

/**
 * Berätta vilka rekommendationer ett specifikt minne påverkar.
 *
 * - Kräver personalization-samtycke (R2).
 * - Använder ENDAST den anropande användarens egna minnen/smaksignaler;
 *   andra hushållsmedlemmars personliga data blir aldrig lästa eller returnerade.
 * - Återanvänder rankAll/scoreCandidate från recommendation-engine.
 */
export async function computeMemoryImpact(options: MemoryImpactOptions): Promise<MemoryImpactResult> {
  const { db, userId, memoryItem, mealType = "dinner", persons, maxMinutes, craving, view = "default", limit = 10 } = options;

  // R2: personalization-samtycke krävs för impact.
  const [consent] = await db
    .select()
    .from(schema.userConsents)
    .where(and(eq(schema.userConsents.userId, userId), eq(schema.userConsents.kind, "personalization")))
    .limit(1);
  if (consent?.status !== "granted") {
    return { memoryItemId: memoryItem.id, personalizationEnabled: false, impacted: [] };
  }

  const today = new Date();
  const householdId = await getActiveHouseholdId(db, userId);

  // --- 1. Lager ---
  const stockRows = householdId
    ? await db
        .select({
          item: schema.inventoryItems,
          locationType: schema.storageLocations.type,
          shelfLife: schema.canonicalIngredients.shelfLifeGuidance,
          density: schema.canonicalIngredients.densityGPerMl,
          gramsPerPiece: schema.canonicalIngredients.gramsPerPiece,
        })
        .from(schema.inventoryItems)
        .innerJoin(
          schema.storageLocations,
          eq(schema.inventoryItems.storageLocationId, schema.storageLocations.id),
        )
        .leftJoin(
          schema.canonicalIngredients,
          eq(schema.inventoryItems.canonicalIngredientId, schema.canonicalIngredients.id),
        )
        .where(
          and(
            eq(schema.inventoryItems.householdId, householdId),
            isNull(schema.inventoryItems.depletedAt),
            gt(schema.inventoryItems.quantity, 0),
          ),
        )
    : [];

  const pantry: PantryItem[] = stockRows.map((r) => ({
    id: r.item.id,
    canonicalIngredientId: r.item.canonicalIngredientId,
    quantity: r.item.quantity,
    unit: r.item.unit,
    bestBeforeDate: r.item.bestBeforeDate,
    useByDate: r.item.useByDate,
    openedAt: r.item.openedAt,
    frozenAt: r.item.frozenAt,
    thawedAt: r.item.thawedAt,
    purchasedAt: r.item.purchasedAt,
    storageLocationType: r.locationType,
    shelfLifeGuidance: r.shelfLife,
  }));
  const unitInfo = new Map(
    stockRows
      .filter((r) => r.item.canonicalIngredientId)
      .map((r) => [
        r.item.canonicalIngredientId!,
        { densityGPerMl: r.density, gramsPerPiece: r.gramsPerPiece },
      ]),
  );

  // --- 2. Hushållets samlade begränsningar (allergener etc är hushållsgemensamma) ---
  const members = householdId
    ? await db
        .select({ userId: schema.householdMembers.userId })
        .from(schema.householdMembers)
        .where(eq(schema.householdMembers.householdId, householdId))
    : [];
  const memberIds = members.length ? members.map((m) => m.userId) : [userId];

  // Läs bara den anropande användarens fulla preferensrad; personliga data från
  // andra hushållsmedlemmar läcker aldrig in i impact-beräkningen.
  const myPrefs = await db
    .select()
    .from(schema.userPreferences)
    .where(eq(schema.userPreferences.userId, userId))
    .limit(1)
    .then((rows) => rows[0]);

  // För säkerhetsfiltrering behöver vi endast de begränsande fälten från alla
  // medlemmar — inte deras personliga preferenser.
  const householdRestrictionRows = await db
    .select({
      allergens: schema.userPreferences.allergens,
      avoidIngredientIds: schema.userPreferences.avoidIngredientIds,
      spiceLevelMax: schema.userPreferences.spiceLevelMax,
    })
    .from(schema.userPreferences)
    .where(inArray(schema.userPreferences.userId, memberIds));

  const combinedAllergens = [...new Set(householdRestrictionRows.flatMap((p) => p.allergens))];
  const combinedAvoid = [...new Set(householdRestrictionRows.flatMap((p) => p.avoidIngredientIds))];
  const strictestSpice = Math.min(...householdRestrictionRows.map((p) => p.spiceLevelMax), 5);

  // --- 3. Kandidater ---
  const candidates = await db
    .select()
    .from(schema.recipes)
    .where(
      and(
        eq(schema.recipes.status, "published"),
        sql`${mealType} = ANY(${schema.recipes.mealTypes})`,
      ),
    )
    .limit(200);

  const allIngredients = await db
    .select()
    .from(schema.recipeIngredients)
    .where(
      inArray(
        schema.recipeIngredients.recipeId,
        candidates.map((c) => c.id),
      ),
    );
  const ingredientIds = [...new Set(allIngredients.map((i) => i.canonicalIngredientId))];
  const safetyRows = await db
    .select()
    .from(schema.canonicalIngredients)
    .where(inArray(schema.canonicalIngredients.id, ingredientIds));
  const safetyMap = new Map<string, IngredientSafetyInfo>(
    safetyRows.map((r) => [
      r.id,
      {
        id: r.id,
        allergens: r.allergens,
        isVegan: r.isVegan,
        isVegetarian: r.isVegetarian,
        containsGluten: r.containsGluten,
        containsLactose: r.containsLactose,
        isPork: r.isPork,
        isBeef: r.isBeef,
        isAlcohol: r.isAlcohol,
      },
    ]),
  );
  for (const r of safetyRows) {
    if (!unitInfo.has(r.id)) {
      unitInfo.set(r.id, { densityGPerMl: r.densityGPerMl, gramsPerPiece: r.gramsPerPiece });
    }
  }

  // --- 4. Näringskontext ---
  const [profile] = await db
    .select()
    .from(schema.userHealthProfiles)
    .where(eq(schema.userHealthProfiles.userId, userId))
    .limit(1);
  const targets =
    profile?.weightKg && profile.heightCm && profile.birthYear
      ? computeDailyTargets({
          sex: profile.sex ?? "unspecified",
          age: today.getUTCFullYear() - profile.birthYear,
          heightCm: profile.heightCm,
          weightKg: profile.weightKg,
          activityLevel: profile.activityLevel,
          primaryGoal: myPrefs?.primaryGoal ?? undefined,
        }).targets
      : DEFAULT_TARGETS;
  const todaysMeals = await db
    .select({ nutrition: schema.meals.nutrition })
    .from(schema.meals)
    .where(and(eq(schema.meals.userId, userId), eq(schema.meals.date, todayIso())));
  const daySummary = summarizeDay(
    todaysMeals.map((m) => m.nutrition),
    targets,
  );

  // --- 5. Säsong & högtid ---
  const events = await db
    .select()
    .from(schema.seasonEvents)
    .where(and(eq(schema.seasonEvents.active, true), eq(schema.seasonEvents.market, "SE")));
  const activeHolidayTags = events
    .filter((e) => isEventActive({ dateRule: e.dateRule, leadDays: e.leadDays }, today))
    .map((e) => e.slug);

  // --- 6. Historik + betyg ---
  const cooks = householdId
    ? await db
        .select({
          recipeId: schema.recipeCooks.recipeId,
          last: sql<string>`max(${schema.recipeCooks.cookedAt})`,
        })
        .from(schema.recipeCooks)
        .where(eq(schema.recipeCooks.householdId, householdId))
        .groupBy(schema.recipeCooks.recipeId)
    : [];
  const lastCooked = new Map(cooks.map((c) => [c.recipeId, c.last]));
  const householdRatings = await db
    .select({
      recipeId: schema.recipeRatings.recipeId,
      avg: sql<number>`avg(${schema.recipeRatings.stars})`,
    })
    .from(schema.recipeRatings)
    .where(inArray(schema.recipeRatings.userId, memberIds))
    .groupBy(schema.recipeRatings.recipeId);
  const householdRatingMap = new Map(householdRatings.map((r) => [r.recipeId, Number(r.avg)]));

  // --- 7. Personliga signaler — ENDAST anroparens egna (R2 + integritet) ---
  const memoryRows = await db
    .select({
      id: schema.memoryItems.id,
      userId: schema.memoryItems.userId,
      householdId: schema.memoryItems.householdId,
      kind: schema.memoryItems.kind,
      key: schema.memoryItems.key,
      summarySv: schema.memoryItems.summarySv,
      value: schema.memoryItems.value,
      origin: schema.memoryItems.origin,
      confidence: schema.memoryItems.confidence,
      verifiedByUser: schema.memoryItems.verifiedByUser,
      paused: schema.memoryItems.paused,
      createdAt: schema.memoryItems.createdAt,
      updatedAt: schema.memoryItems.updatedAt,
    })
    .from(schema.memoryItems)
    .where(eq(schema.memoryItems.userId, userId));
  const userMemoryItems: MemoryItem[] = memoryRows.map((m) => ({
    ...m,
    userId: m.userId ?? undefined,
    householdId: m.householdId ?? undefined,
    createdAt: m.createdAt.toISOString(),
    updatedAt: m.updatedAt.toISOString(),
    lastUsedAt: undefined,
    expiresAt: undefined,
  }));

  const tasteRows = await db
    .select({
      id: schema.tasteSignals.id,
      userId: schema.tasteSignals.userId,
      axis: schema.tasteSignals.axis,
      target: schema.tasteSignals.target,
      direction: schema.tasteSignals.direction,
      strength: schema.tasteSignals.strength,
      origin: schema.tasteSignals.origin,
      refRecipeId: schema.tasteSignals.refRecipeId,
      createdAt: schema.tasteSignals.createdAt,
    })
    .from(schema.tasteSignals)
    .where(eq(schema.tasteSignals.userId, userId));
  const tasteSignals: TasteSignal[] = tasteRows.map((t) => ({
    ...t,
    target: t.target ?? undefined,
    refRecipeId: t.refRecipeId ?? undefined,
    createdAt: t.createdAt.toISOString(),
  }));

  let cookingAssumptions: CookingAssumption[] = [];
  if (householdId) {
    const assumptions = await db
      .select({
        canonicalIngredientId: schema.cookingAssumptionProfiles.canonicalIngredientId,
        averageEatenPortions: schema.cookingAssumptionProfiles.averageEatenPortions,
        averageLeftoverPortions: schema.cookingAssumptionProfiles.averageLeftoverPortions,
        observationCount: schema.cookingAssumptionProfiles.observationCount,
      })
      .from(schema.cookingAssumptionProfiles)
      .where(eq(schema.cookingAssumptionProfiles.householdId, householdId));
    cookingAssumptions = assumptions.map((a) => ({
      canonicalIngredientId: a.canonicalIngredientId,
      averageEatenPortions: a.averageEatenPortions,
      averageLeftoverPortions: a.averageLeftoverPortions,
      observationCount: a.observationCount,
    }));
  }

  // --- 8. Bygg kandidater ---
  const scoredCandidates: RecommendationCandidate[] = [];
  for (const recipe of candidates) {
    const recipeIngredients = allIngredients
      .filter((i) => i.recipeId === recipe.id)
      .map((i) => ({
        canonicalIngredientId: i.canonicalIngredientId,
        displayNameSv: i.displayNameSv,
        quantity: i.quantity,
        unit: i.unit,
        optional: i.optional,
      }));

    const violations = checkRecipeSafety(
      {
        ingredients: recipeIngredients.map((i) => ({
          canonicalIngredientId: i.canonicalIngredientId,
          optional: i.optional,
        })),
        spiceLevel: recipe.spiceLevel,
      },
      {
        allergens: combinedAllergens,
        dietPattern: myPrefs?.dietPattern,
        religiousRule: myPrefs?.religiousRule,
        avoidIngredientIds: combinedAvoid,
        spiceLevelMax: strictestSpice,
      },
      safetyMap,
    );
    if (!isRecipeSafe(violations)) continue;

    const coverage = computeCoverage(recipeIngredients, pantry, unitInfo, today);
    const lastDate = lastCooked.get(recipe.id);
    scoredCandidates.push({
      recipeId: recipe.id,
      titleSv: recipe.titleSv,
      cuisine: recipe.cuisine,
      tags: recipe.tags as never,
      totalTimeMinutes: recipe.totalTimeMinutes,
      nutritionPerPortion: recipe.nutritionPerPortion,
      estimatedCostMinorPerPortion: recipe.estimatedCostMinorPerPortion,
      ratingAverage: recipe.ratingAverage,
      ratingCount: recipe.ratingCount,
      peakSeasons: recipe.peakSeasons,
      holidayTags: recipe.holidayTags,
      spiceLevel: recipe.spiceLevel,
      coverage,
      daysSinceLastCooked: lastDate
        ? Math.floor((today.getTime() - Date.parse(lastDate)) / 86_400_000)
        : null,
      householdRating: householdRatingMap.get(recipe.id) ?? null,
      ingredientIds: recipeIngredients
        .map((i) => i.canonicalIngredientId)
        .filter((id): id is string => id != null),
    });
  }

  const parsedCraving = craving ? parseCraving(craving) : null;
  const baseCtx: RecommendationContext = {
    mealType: mealType as never,
    persons: persons ?? (members.length ? members.length : 1),
    maxMinutes,
    remainingProteinG: Math.max(0, daySummary.remaining.proteinG),
    remainingKcal: Math.max(0, daySummary.remaining.kcal),
    currentSeason: seasonForDate(today),
    activeHolidayTags,
    isWeekday: today.getUTCDay() >= 1 && today.getUTCDay() <= 4,
    favoriteCuisines: myPrefs?.favoriteCuisines ?? [],
    cravingTags: parsedCraving?.tags,
    cravingCuisine: parsedCraving?.cuisine,
    cravingMaxKcal: parsedCraving?.maxKcal,
    personalizationEnabled: true,
    memoryItems: userMemoryItems,
    tasteSignals,
    cookingAssumptions,
  };

  const weights = view === "default" ? undefined : viewWeights(view as RecommendationView);
  // Använd default-vikter för impact eftersom vi vill se minnets effekt i normalfallet.
  const withMemory = rankAll(scoredCandidates, baseCtx, weights, limit * 2);

  const withoutCtx: RecommendationContext = {
    ...baseCtx,
    memoryItems: userMemoryItems.filter((m) => m.id !== memoryItem.id),
  };
  const withoutMemory = rankAll(scoredCandidates, withoutCtx, weights, limit * 2);

  const withoutById = new Map(withoutMemory.map((r) => [r.recipeId, r]));
  const impacted: MemoryImpactResult["impacted"] = [];

  for (const rec of withMemory) {
    const without = withoutById.get(rec.recipeId);
    if (!without) continue;
    const delta = Math.round((rec.score - without.score) * 10) / 10;
    if (delta === 0) continue;
    impacted.push({
      recipeId: rec.recipeId,
      titleSv: rec.titleSv,
      scoreWith: rec.score,
      scoreWithout: without.score,
      delta,
      whySv: rec.whySv,
      provenance: rec.provenance,
    });
  }

  impacted.sort((a, b) => b.delta - a.delta);

  return {
    memoryItemId: memoryItem.id,
    personalizationEnabled: true,
    impacted: impacted.slice(0, limit),
  };
}
