import type {
  RecommendationCandidate,
  RecommendationContext,
  ScoredRecommendation,
  ScoringWeights,
} from "./types.js";
import { DEFAULT_WEIGHTS } from "./types.js";
import { buildWhySv } from "./explain.js";

/**
 * Poängsätt en kandidat (0–100-skala per delkomponent, viktad summa).
 * Helt deterministisk: samma indata → samma poäng → förklarbar rekommendation.
 */
export function scoreCandidate(
  candidate: RecommendationCandidate,
  ctx: RecommendationContext,
  weights: ScoringWeights = DEFAULT_WEIGHTS,
): ScoredRecommendation {
  const parts: Record<string, number> = {};

  // 1. Ingredienstäckning – kärnan i "utgå från vad som finns hemma".
  parts.coverage = candidate.coverage.coverage;

  // 2. Utgångsdatum: störst poäng när receptet räddar varor som snart går ut.
  const expiring = candidate.coverage.expiringUsed;
  if (expiring.length === 0) {
    parts.expiry = 0;
  } else {
    const mostUrgent = Math.min(
      ...expiring.map((m) => (m.mostUrgentDaysLeft == null ? 99 : m.mostUrgentDaysLeft)),
    );
    parts.expiry = mostUrgent <= 1 ? 1 : mostUrgent <= 3 ? 0.8 : 0.5;
  }

  // 3. Näringsfit mot återstående dagsmål.
  parts.nutritionFit = nutritionFit(candidate, ctx);

  // 4. Smak: favoritkök + styrka nära preferens.
  parts.taste = ctx.favoriteCuisines.includes(candidate.cuisine) ? 1 : 0.4;

  // 5. Betyg: hushållets egna betyg väger tyngst, annars community (kräver volym).
  if (candidate.householdRating != null) {
    parts.rating = clamp01((candidate.householdRating - 2.5) / 2.5);
  } else if (candidate.ratingAverage != null && candidate.ratingCount >= 5) {
    parts.rating = clamp01((candidate.ratingAverage - 3) / 2);
  } else {
    parts.rating = 0.5;
  }

  // 6–7. Säsong & högtid.
  parts.season = candidate.peakSeasons.includes(ctx.currentSeason) ? 1 : 0.3;
  parts.holiday =
    ctx.activeHolidayTags.length > 0 &&
    candidate.holidayTags.some((t) => ctx.activeHolidayTags.includes(t))
      ? 1
      : 0;

  // 8. Tid: hård maxgräns om satt, annars vardagsbonus för snabbt.
  if (ctx.maxMinutes != null && candidate.totalTimeMinutes > ctx.maxMinutes) {
    parts.time = -1; // diskvalificerande straff hanteras i rankAll
  } else if (ctx.isWeekday) {
    parts.time =
      candidate.totalTimeMinutes <= 25 ? 1 : candidate.totalTimeMinutes <= 45 ? 0.6 : 0.2;
  } else {
    parts.time = candidate.totalTimeMinutes <= 90 ? 0.7 : 0.5;
  }

  // 9. Budget.
  const cost = candidate.estimatedCostMinorPerPortion;
  if (ctx.maxCostMinorPerPortion != null && cost != null) {
    parts.budget = cost <= ctx.maxCostMinorPerPortion ? 1 : -0.5;
  } else if (cost != null) {
    parts.budget = cost <= 25 ? 1 : cost <= 45 ? 0.6 : 0.3;
  } else {
    parts.budget = 0.4;
  }

  // 10. Variation: nyligen lagat straffas (spec §25: variation).
  const days = candidate.daysSinceLastCooked;
  parts.variety = days == null ? 0.8 : days < 7 ? 0 : days < 14 ? 0.4 : 1;

  // 11. Väder (spec §29): varmt → grill/sallad, kallt → gryta/soppa.
  parts.weather = weatherFit(candidate, ctx);

  // 12. "Jag är sugen på" (spec §19).
  parts.craving = cravingFit(candidate, ctx);

  const score = weightedSum(parts, weights);

  return {
    recipeId: candidate.recipeId,
    titleSv: candidate.titleSv,
    score: Math.round(score * 10) / 10,
    parts,
    whySv: buildWhySv(candidate, ctx, parts),
    missingIngredients: candidate.coverage.missing
      .filter((m) => !m.optional)
      .map((m) => m.displayNameSv),
    usesExpiring: expiring.map((m) => ({
      nameSv: m.displayNameSv,
      daysLeft: m.mostUrgentDaysLeft,
    })),
    coveragePercent: Math.round(candidate.coverage.coverage * 100),
  };
}

/** Ranka alla kandidater; kandidater över tidsgränsen filtreras bort. */
export function rankAll(
  candidates: RecommendationCandidate[],
  ctx: RecommendationContext,
  weights: ScoringWeights = DEFAULT_WEIGHTS,
  limit = 5,
): ScoredRecommendation[] {
  return candidates
    .map((c) => scoreCandidate(c, ctx, weights))
    .filter((s) => (s.parts.time ?? 0) >= 0)
    .sort((a, b) => b.score - a.score)
    .slice(0, limit);
}

function nutritionFit(candidate: RecommendationCandidate, ctx: RecommendationContext): number {
  let fit = 0.5;
  const protein = candidate.nutritionPerPortion.proteinG;
  if (ctx.remainingProteinG != null && ctx.remainingProteinG > 0) {
    // Ju närmare receptet fyller proteinluckan, desto bättre (upp till 1).
    fit = clamp01(protein / Math.max(20, ctx.remainingProteinG * 0.6));
  }
  if (ctx.isTrainingDay && protein >= 35) fit = Math.min(1, fit + 0.3);
  if (ctx.remainingKcal != null && ctx.remainingKcal > 0) {
    const kcal = candidate.nutritionPerPortion.kcal;
    if (kcal > ctx.remainingKcal * 1.3) fit *= 0.5;
  }
  return clamp01(fit);
}

function weatherFit(candidate: RecommendationCandidate, ctx: RecommendationContext): number {
  if (!ctx.weather || ctx.weather === "unknown") return 0.5;
  const title = candidate.titleSv.toLowerCase();
  const has = (words: string[]) =>
    words.some((w) => title.includes(w)) ||
    candidate.tags.some((t) => words.includes(t)) ||
    words.includes(candidate.cuisine);
  switch (ctx.weather) {
    case "hot":
    case "warm":
      return has(["grill", "sallad", "kall", "bowl", "wrap"]) ? 1 : 0.4;
    case "cold":
    case "snow":
      return has(["soppa", "gryta", "långkok", "ugns", "pytt"]) ? 1 : 0.4;
    case "rain":
      return has(["gryta", "långkok", "soppa", "paj"]) ? 0.9 : 0.5;
    default:
      return 0.5;
  }
}

function cravingFit(candidate: RecommendationCandidate, ctx: RecommendationContext): number {
  const tags = ctx.cravingTags ?? [];
  if (tags.length === 0 && !ctx.cravingCuisine && ctx.cravingMaxKcal == null) return 0.5;
  let hits = 0;
  let checks = 0;

  if (ctx.cravingCuisine) {
    checks += 1;
    if (candidate.cuisine === ctx.cravingCuisine) hits += 1;
  }
  if (ctx.cravingMaxKcal != null) {
    checks += 1;
    if (candidate.nutritionPerPortion.kcal <= ctx.cravingMaxKcal) hits += 1;
  }
  if (tags.length > 0) {
    checks += 1;
    const title = candidate.titleSv.toLowerCase();
    const candidateTags = new Set<string>(candidate.tags);
    const matched = tags.some(
      (t) =>
        candidateTags.has(t) ||
        title.includes(t) ||
        (t === "spicy" && candidate.spiceLevel >= 3) ||
        (t === "quick" && candidate.totalTimeMinutes <= 25) ||
        (t === "high_protein" && candidate.nutritionPerPortion.proteinG >= 35),
    );
    if (matched) hits += 1;
  }

  return checks === 0 ? 0.5 : hits / checks;
}

function weightedSum(parts: Record<string, number>, weights: ScoringWeights): number {
  let total = 0;
  for (const [key, value] of Object.entries(parts)) {
    const weight = (weights as unknown as Record<string, number>)[key] ?? 0;
    total += clampPart(value) * weight;
  }
  return total;
}

function clamp01(v: number): number {
  return Math.max(0, Math.min(1, v));
}
function clampPart(v: number): number {
  return Math.max(-1, Math.min(1, v));
}
