import type { FastifyInstance } from "fastify";
import { and, eq, gt, inArray, isNull, sql } from "drizzle-orm";
import { schema } from "@app/database";
import type { StoreSection } from "@app/shared-types";
import {
  addShoppingItemInputSchema,
  completeShoppingInputSchema,
  createShoppingListInputSchema,
  idParamSchema,
  updateShoppingItemInputSchema,
} from "@app/validation";
import { convert } from "@app/nutrition-engine";
import { errors, parse } from "../lib/errors.js";
import { emitEvent, requireActiveHousehold, requireMembership, todayIso } from "../lib/helpers.js";

/**
 * Inköpslista (spec §27): dra av lager, slå ihop ingredienser, sortera per
 * butiksavdelning, dela i hushållet, uppdatera lagret efter köp.
 */

const CATEGORY_TO_SECTION: Record<string, StoreSection> = {
  mejeri: "mejeri",
  kott_fagel: "kott_fagel",
  fisk: "fisk",
  gronsaker: "frukt_gront",
  frukt: "frukt_gront",
  spannmal: "skafferi",
  baljvaxter: "skafferi",
  skafferi: "skafferi",
  konserver: "konserver",
  kryddor: "kryddor_bak",
  brod: "brod",
};

export async function shoppingRoutes(app: FastifyInstance) {
  const auth = { preHandler: [app.authenticate] };

  app.get("/v1/shopping-lists", auth, async (req) => {
    const householdId = await requireActiveHousehold(app.db, req.userId);
    const lists = await app.db
      .select()
      .from(schema.shoppingLists)
      .where(
        and(
          eq(schema.shoppingLists.householdId, householdId),
          eq(schema.shoppingLists.status, "active"),
        ),
      );
    return { lists };
  });

  app.post("/v1/shopping-lists", auth, async (req, reply) => {
    const input = parse(createShoppingListInputSchema, req.body);
    const householdId = await requireActiveHousehold(app.db, req.userId);

    const [list] = await app.db
      .insert(schema.shoppingLists)
      .values({ householdId, name: input.name, weekPlanId: input.weekPlanId ?? null })
      .returning();

    // Generera från veckoplan: receptbehov − befintligt lager (spec §27)
    if (input.generateFromPlan && input.weekPlanId) {
      await generateItemsFromPlan(app, list!.id, input.weekPlanId, householdId, req.userId);
    }

    const items = await app.db
      .select()
      .from(schema.shoppingListItems)
      .where(eq(schema.shoppingListItems.shoppingListId, list!.id));
    return reply.status(201).send({ list, items });
  });

  app.get("/v1/shopping-lists/:id", auth, async (req) => {
    const { id } = parse(idParamSchema, req.params);
    const list = await getOwnedList(app, id, req.userId);
    const items = await app.db
      .select()
      .from(schema.shoppingListItems)
      .where(eq(schema.shoppingListItems.shoppingListId, id))
      .orderBy(schema.shoppingListItems.storeSection, schema.shoppingListItems.sortOrder);
    const estimatedTotal = items.reduce((sum, i) => sum + (i.estimatedPriceMinor ?? 0), 0);
    // Prisuppskattningar härleds ur katalogens baspriser (SEK) tills per-marknads-priser (M8).
    return { list, items, estimatedTotalMinor: Math.round(estimatedTotal), currency: "SEK" };
  });

  app.post("/v1/shopping-lists/:id/items", auth, async (req, reply) => {
    const { id } = parse(idParamSchema, req.params);
    await getOwnedList(app, id, req.userId);
    const input = parse(addShoppingItemInputSchema, req.body);

    let section = input.storeSection;
    let estimatedPrice = input.estimatedPriceMinor;
    if (input.canonicalIngredientId) {
      const [ing] = await app.db
        .select()
        .from(schema.canonicalIngredients)
        .where(eq(schema.canonicalIngredients.id, input.canonicalIngredientId))
        .limit(1);
      if (ing) {
        section = section ?? CATEGORY_TO_SECTION[ing.category] ?? "hygien_ovrigt";
        if (estimatedPrice == null && ing.defaultPriceMinorPerKg != null) {
          const grams = convert(input.quantity, input.unit, "GRAM", {
            densityGPerMl: ing.densityGPerMl,
            gramsPerPiece: ing.gramsPerPiece,
          });
          if (grams != null)
            estimatedPrice = Math.round((grams / 1000) * ing.defaultPriceMinorPerKg * 10) / 10;
        }
      }
    }

    // Slå ihop med befintlig rad för samma ingrediens (spec §27)
    if (input.canonicalIngredientId) {
      const [existing] = await app.db
        .select()
        .from(schema.shoppingListItems)
        .where(
          and(
            eq(schema.shoppingListItems.shoppingListId, id),
            eq(schema.shoppingListItems.canonicalIngredientId, input.canonicalIngredientId),
            eq(schema.shoppingListItems.unit, input.unit),
            eq(schema.shoppingListItems.checked, false),
          ),
        )
        .limit(1);
      if (existing) {
        const [merged] = await app.db
          .update(schema.shoppingListItems)
          .set({
            quantity: existing.quantity + input.quantity,
            estimatedPriceMinor:
              existing.estimatedPriceMinor != null && estimatedPrice != null
                ? existing.estimatedPriceMinor + estimatedPrice
                : (existing.estimatedPriceMinor ?? estimatedPrice ?? null),
          })
          .where(eq(schema.shoppingListItems.id, existing.id))
          .returning();
        return reply.send({ item: merged, merged: true });
      }
    }

    const [item] = await app.db
      .insert(schema.shoppingListItems)
      .values({
        shoppingListId: id,
        canonicalIngredientId: input.canonicalIngredientId ?? null,
        displayName: input.displayName,
        quantity: input.quantity,
        unit: input.unit,
        storeSection: section ?? "hygien_ovrigt",
        estimatedPriceMinor: estimatedPrice ?? null,
        addedByUserId: req.userId,
        origin: "manual",
      })
      .returning();
    return reply.status(201).send({ item, merged: false });
  });

  app.patch("/v1/shopping-lists/:id/items/:itemId", auth, async (req) => {
    const params = req.params as { id: string; itemId: string };
    await getOwnedList(app, params.id, req.userId);
    const input = parse(updateShoppingItemInputSchema, req.body);
    const [item] = await app.db
      .update(schema.shoppingListItems)
      .set(input)
      .where(
        and(
          eq(schema.shoppingListItems.id, params.itemId),
          eq(schema.shoppingListItems.shoppingListId, params.id),
        ),
      )
      .returning();
    if (!item) throw errors.notFound();
    return item;
  });

  app.delete("/v1/shopping-lists/:id/items/:itemId", auth, async (req) => {
    const params = req.params as { id: string; itemId: string };
    await getOwnedList(app, params.id, req.userId);
    await app.db
      .delete(schema.shoppingListItems)
      .where(
        and(
          eq(schema.shoppingListItems.id, params.itemId),
          eq(schema.shoppingListItems.shoppingListId, params.id),
        ),
      );
    return { ok: true };
  });

  /** Avsluta köprundan: bockade varor in i lagret (spec §27). */
  app.post("/v1/shopping-lists/:id/complete", auth, async (req) => {
    const { id } = parse(idParamSchema, req.params);
    const list = await getOwnedList(app, id, req.userId);
    const input = parse(completeShoppingInputSchema, req.body);

    const items = await app.db
      .select()
      .from(schema.shoppingListItems)
      .where(
        and(
          eq(schema.shoppingListItems.shoppingListId, id),
          eq(schema.shoppingListItems.checked, true),
        ),
      );

    let added = 0;
    if (input.addToInventory && items.length > 0) {
      const overrides = new Map(input.storageDefaults.map((s) => [s.shoppingListItemId, s]));
      const fallback =
        input.defaultStorageLocationId ??
        (
          await app.db
            .select({ id: schema.storageLocations.id })
            .from(schema.storageLocations)
            .where(
              and(
                eq(schema.storageLocations.householdId, list.householdId),
                eq(schema.storageLocations.type, "fridge"),
              ),
            )
            .limit(1)
        )[0]?.id;
      if (!fallback) throw errors.badRequest("Ingen standardplats (kyl) hittades i hushållet.");

      for (const item of items) {
        const override = overrides.get(item.id);
        const [inv] = await app.db
          .insert(schema.inventoryItems)
          .values({
            householdId: list.householdId,
            canonicalIngredientId: item.canonicalIngredientId,
            displayName: item.displayName,
            quantity: item.quantity,
            unit: item.unit,
            storageLocationId: override?.storageLocationId ?? fallback,
            purchasedAt: todayIso(),
            bestBeforeDate: override?.bestBeforeDate ?? null,
            priceMinor: override?.priceMinor ?? item.estimatedPriceMinor,
            source: "manual_search",
            confidence: 1,
            verifiedByUser: true,
            lastVerifiedAt: new Date(),
          })
          .returning();
        await app.db.insert(schema.inventoryTransactions).values({
          householdId: list.householdId,
          inventoryItemId: inv!.id,
          type: "purchase",
          quantityDelta: item.quantity,
          unit: item.unit,
          refType: "shopping",
          refId: id,
          actorUserId: req.userId,
          valueMinor: override?.priceMinor ?? item.estimatedPriceMinor,
        });
        added += 1;
      }
    }

    await app.db
      .update(schema.shoppingLists)
      .set({ status: "completed", updatedAt: new Date() })
      .where(eq(schema.shoppingLists.id, id));

    await emitEvent(app.db, {
      type: "SHOPPING_COMPLETED",
      payload: { shoppingListId: id, itemsAdded: added },
      userId: req.userId,
      householdId: list.householdId,
      correlationId: req.correlationId,
    });
    return { ok: true, itemsAddedToInventory: added };
  });
}

async function getOwnedList(app: FastifyInstance, listId: string, userId: string) {
  const [list] = await app.db
    .select()
    .from(schema.shoppingLists)
    .where(eq(schema.shoppingLists.id, listId))
    .limit(1);
  if (!list) throw errors.notFound("Listan finns inte.");
  await requireMembership(app.db, list.householdId, userId);
  return list;
}

/** Aggregera receptbehov från plan, dra av befintligt lager, skapa rader. */
async function generateItemsFromPlan(
  app: FastifyInstance,
  listId: string,
  weekPlanId: string,
  householdId: string,
  userId: string,
) {
  const entries = await app.db
    .select()
    .from(schema.weekPlanEntries)
    .where(
      and(
        eq(schema.weekPlanEntries.weekPlanId, weekPlanId),
        eq(schema.weekPlanEntries.status, "planned"),
      ),
    );

  const recipeIds = [...new Set(entries.filter((e) => e.recipeId).map((e) => e.recipeId!))];
  if (recipeIds.length === 0) return;

  const allIngredients = await app.db
    .select()
    .from(schema.recipeIngredients)
    .where(inArray(schema.recipeIngredients.recipeId, recipeIds));
  const recipes = await app.db
    .select({ id: schema.recipes.id, portions: schema.recipes.portions })
    .from(schema.recipes)
    .where(inArray(schema.recipes.id, recipeIds));
  const portionsMap = new Map(recipes.map((r) => [r.id, r.portions]));

  // Aggregera behov per ingrediens (i gram där möjligt)
  const needs = new Map<string, { name: string; grams: number }>();
  const infoRows = await app.db
    .select()
    .from(schema.canonicalIngredients)
    .where(
      inArray(schema.canonicalIngredients.id, [
        ...new Set(allIngredients.map((i) => i.canonicalIngredientId)),
      ]),
    );
  const infoMap = new Map(infoRows.map((r) => [r.id, r]));

  for (const entry of entries) {
    if (!entry.recipeId) continue;
    const basePortions = portionsMap.get(entry.recipeId) ?? 4;
    const factor = entry.portions / basePortions;
    for (const ing of allIngredients.filter((i) => i.recipeId === entry.recipeId && !i.optional)) {
      const info = infoMap.get(ing.canonicalIngredientId);
      const grams = convert(ing.quantity * factor, ing.unit, "GRAM", {
        densityGPerMl: info?.densityGPerMl,
        gramsPerPiece: info?.gramsPerPiece,
      });
      if (grams == null) continue;
      const current = needs.get(ing.canonicalIngredientId) ?? { name: ing.displayNameSv, grams: 0 };
      current.grams += grams;
      needs.set(ing.canonicalIngredientId, current);
    }
  }

  // Dra av lager
  const stock = await app.db
    .select()
    .from(schema.inventoryItems)
    .where(
      and(
        eq(schema.inventoryItems.householdId, householdId),
        isNull(schema.inventoryItems.depletedAt),
        gt(schema.inventoryItems.quantity, 0),
        inArray(schema.inventoryItems.canonicalIngredientId, [...needs.keys()]),
      ),
    );
  for (const item of stock) {
    if (!item.canonicalIngredientId) continue;
    const need = needs.get(item.canonicalIngredientId);
    if (!need) continue;
    const info = infoMap.get(item.canonicalIngredientId);
    const grams = convert(item.quantity, item.unit, "GRAM", {
      densityGPerMl: info?.densityGPerMl,
      gramsPerPiece: info?.gramsPerPiece,
    });
    if (grams != null) need.grams = Math.max(0, need.grams - grams);
  }

  // Skapa rader för det som saknas
  let sortOrder = 0;
  for (const [ingredientId, need] of needs) {
    if (need.grams < 5) continue;
    const info = infoMap.get(ingredientId);
    const section = info
      ? (CATEGORY_TO_SECTION[info.category] ?? "hygien_ovrigt")
      : "hygien_ovrigt";
    // Konvertera tillbaka till naturlig enhet
    const targetUnit = info?.defaultUnit ?? "GRAM";
    const qty =
      convert(need.grams, "GRAM", targetUnit, {
        densityGPerMl: info?.densityGPerMl,
        gramsPerPiece: info?.gramsPerPiece,
      }) ?? need.grams;
    const rounded = targetUnit === "COUNT" ? Math.ceil(qty) : Math.ceil(qty * 10) / 10;
    const estimatedPrice =
      info?.defaultPriceMinorPerKg != null
        ? Math.round((need.grams / 1000) * info.defaultPriceMinorPerKg * 10) / 10
        : null;

    await app.db.insert(schema.shoppingListItems).values({
      shoppingListId: listId,
      canonicalIngredientId: ingredientId,
      displayName: need.name,
      quantity: rounded,
      unit: targetUnit,
      storeSection: section,
      estimatedPriceMinor: estimatedPrice,
      addedByUserId: userId,
      origin: "plan",
      sortOrder: sortOrder++,
    });
  }
}
