import type { FastifyInstance } from "fastify";
import { and, desc, eq, gt, ilike, isNull, or } from "drizzle-orm";
import { schema } from "@app/database";
import {
  createInventoryItemInputSchema,
  idParamSchema,
  inventoryQuerySchema,
  inventoryTransactionInputSchema,
  updateInventoryItemInputSchema,
} from "@app/validation";
import { classifyExpiry, findDuplicateCandidates, normalizeDelta } from "@app/inventory-engine";
import { errors, parse } from "../lib/errors.js";
import { emitEvent, requireActiveHousehold, requireMembership } from "../lib/helpers.js";

/**
 * Food Twin – lagret (spec §8). Transaktionsbaserat: varje förändring skrivs
 * som inventory_transaction och saldot uppdateras atomiskt.
 */
export async function inventoryRoutes(app: FastifyInstance) {
  const auth = { preHandler: [app.authenticate] };

  app.get("/v1/inventory", auth, async (req) => {
    const query = parse(inventoryQuerySchema, req.query);
    const householdId = await requireActiveHousehold(app.db, req.userId);

    const conditions = [
      eq(schema.inventoryItems.householdId, householdId),
      isNull(schema.inventoryItems.depletedAt),
      gt(schema.inventoryItems.quantity, 0),
    ];
    if (query.storageLocationId) {
      conditions.push(eq(schema.inventoryItems.storageLocationId, query.storageLocationId));
    }
    if (query.search) {
      const pattern = `%${query.search}%`;
      conditions.push(
        or(
          ilike(schema.inventoryItems.displayName, pattern),
          ilike(schema.inventoryItems.brand, pattern),
        )!,
      );
    }

    const rows = await app.db
      .select({
        item: schema.inventoryItems,
        locationType: schema.storageLocations.type,
        locationName: schema.storageLocations.name,
        shelfLife: schema.canonicalIngredients.shelfLifeGuidance,
      })
      .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(...conditions))
      .orderBy(desc(schema.inventoryItems.updatedAt))
      .limit(query.limit)
      .offset(query.offset);

    const items = rows.map((r) => {
      const expiry = classifyExpiry({
        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,
      });
      return {
        ...r.item,
        locationName: r.locationName,
        locationType: r.locationType,
        expiry,
      };
    });

    const filtered = query.expiryStatus
      ? items.filter((i) => i.expiry.status === query.expiryStatus)
      : items;
    return { items: filtered };
  });

  /** Varor som bör användas snart – driver "använd först" (spec §4.4, §40). */
  app.get("/v1/inventory/expiring", auth, async (req) => {
    const householdId = await requireActiveHousehold(app.db, req.userId);
    const rows = await app.db
      .select({
        item: schema.inventoryItems,
        locationType: schema.storageLocations.type,
        shelfLife: schema.canonicalIngredients.shelfLifeGuidance,
      })
      .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 withExpiry = rows
      .map((r) => ({
        ...r.item,
        expiry: classifyExpiry({
          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,
        }),
      }))
      .filter(
        (i) =>
          i.expiry.status === "expiring" ||
          i.expiry.status === "use_soon" ||
          i.expiry.status === "expired",
      )
      .sort((a, b) => (a.expiry.daysLeft ?? 99) - (b.expiry.daysLeft ?? 99));

    return { items: withExpiry };
  });

  app.post("/v1/inventory/items", auth, async (req, reply) => {
    const input = parse(createInventoryItemInputSchema, req.body);
    const householdId = await requireActiveHousehold(app.db, req.userId);

    const [location] = await app.db
      .select()
      .from(schema.storageLocations)
      .where(
        and(
          eq(schema.storageLocations.id, input.storageLocationId),
          eq(schema.storageLocations.householdId, householdId),
        ),
      )
      .limit(1);
    if (!location) throw errors.badRequest("Förvaringsplatsen tillhör inte ditt hushåll.");

    // Dubblettkontroll (spec §9) – varna, blockera inte.
    const existing = await app.db
      .select()
      .from(schema.inventoryItems)
      .where(
        and(
          eq(schema.inventoryItems.householdId, householdId),
          isNull(schema.inventoryItems.depletedAt),
          gt(schema.inventoryItems.quantity, 0),
        ),
      )
      .limit(200);
    const duplicates = findDuplicateCandidates(
      {
        canonicalIngredientId: input.canonicalIngredientId ?? null,
        displayName: input.displayName,
        brand: input.brand ?? null,
        quantity: input.quantity,
        source: input.source,
        createdAt: new Date().toISOString(),
      },
      existing.map((e) => ({
        id: e.id,
        canonicalIngredientId: e.canonicalIngredientId,
        displayName: e.displayName,
        brand: e.brand,
        quantity: e.quantity,
        source: e.source,
        createdAt: e.createdAt.toISOString(),
      })),
    );

    const [item] = await app.db
      .insert(schema.inventoryItems)
      .values({
        householdId,
        canonicalIngredientId: input.canonicalIngredientId ?? null,
        productId: input.productId ?? null,
        displayName: input.displayName,
        brand: input.brand ?? null,
        quantity: input.quantity,
        unit: input.unit,
        storageLocationId: input.storageLocationId,
        sublocation: input.sublocation ?? null,
        purchasedAt: input.purchasedAt ?? null,
        openedAt: input.openedAt ?? null,
        bestBeforeDate: input.bestBeforeDate ?? null,
        useByDate: input.useByDate ?? null,
        dateKind: input.dateKind ?? null,
        frozenAt: input.frozenAt ?? null,
        priceMinor: input.priceMinor ?? null,
        source: input.source,
        confidence: input.source === "manual_search" || input.source === "free_text" ? 1 : 0.9,
        verifiedByUser: true,
        lastVerifiedAt: new Date(),
      })
      .returning();

    await app.db.insert(schema.inventoryTransactions).values({
      householdId,
      inventoryItemId: item!.id,
      type: "purchase",
      quantityDelta: input.quantity,
      unit: input.unit,
      refType: "manual",
      actorUserId: req.userId,
      valueMinor: input.priceMinor ?? null,
    });

    await emitEvent(app.db, {
      type: "PRODUCT_ADDED",
      payload: {
        inventoryItemId: item!.id,
        canonicalIngredientId: input.canonicalIngredientId ?? null,
        quantity: input.quantity,
        unit: input.unit,
        source: input.source,
      },
      userId: req.userId,
      householdId,
      correlationId: req.correlationId,
    });

    return reply.status(201).send({ item, duplicateCandidates: duplicates });
  });

  app.patch("/v1/inventory/items/:id", auth, async (req) => {
    const { id } = parse(idParamSchema, req.params);
    const input = parse(updateInventoryItemInputSchema, req.body);
    const item = await getOwnedItem(app, id, req.userId);

    const { verifiedByUser, quantity, ...fields } = input;
    const updates: Record<string, unknown> = { ...fields, updatedAt: new Date() };
    if (verifiedByUser) {
      updates.verifiedByUser = true;
      updates.lastVerifiedAt = new Date();
    }

    // Mängdändring går ALLTID via transaktion (spec §8).
    if (quantity != null && quantity !== item.quantity) {
      const delta = quantity - item.quantity;
      await app.db.insert(schema.inventoryTransactions).values({
        householdId: item.householdId,
        inventoryItemId: id,
        type: "adjust",
        quantityDelta: delta,
        unit: item.unit,
        refType: "manual",
        actorUserId: req.userId,
        note: "Manuell justering",
      });
      updates.quantity = quantity;
    }

    const [row] = await app.db
      .update(schema.inventoryItems)
      .set(updates)
      .where(eq(schema.inventoryItems.id, id))
      .returning();

    await emitEvent(app.db, {
      type: "PRODUCT_UPDATED",
      payload: { inventoryItemId: id, changes: updates },
      userId: req.userId,
      householdId: item.householdId,
      correlationId: req.correlationId,
    });
    return row;
  });

  /** Konsumera/släng/justera – kärnan i transaktionsmodellen. */
  app.post("/v1/inventory/items/:id/transactions", auth, async (req) => {
    const { id } = parse(idParamSchema, req.params);
    const input = parse(inventoryTransactionInputSchema, req.body);
    const item = await getOwnedItem(app, id, req.userId);

    const delta = normalizeDelta(input.type, input.quantityDelta);
    const newQuantity = Math.max(0, Math.round((item.quantity + delta) * 1000) / 1000);
    const actualDelta = newQuantity - item.quantity;

    const valueMinor =
      input.type === "discard" && item.priceMinor != null && item.quantity > 0
        ? Math.round(Math.abs(actualDelta / item.quantity) * item.priceMinor * 100) / 100
        : null;

    await app.db.insert(schema.inventoryTransactions).values({
      householdId: item.householdId,
      inventoryItemId: id,
      type: input.type,
      quantityDelta: actualDelta,
      unit: item.unit,
      refType: "manual",
      actorUserId: req.userId,
      note: input.note ?? null,
      valueMinor,
    });

    const [updated] = await app.db
      .update(schema.inventoryItems)
      .set({
        quantity: newQuantity,
        depletedAt: newQuantity <= 0 ? new Date() : null,
        updatedAt: new Date(),
      })
      .where(eq(schema.inventoryItems.id, id))
      .returning();

    const eventType =
      input.type === "discard"
        ? "PRODUCT_DISCARDED"
        : input.type === "consume"
          ? "PRODUCT_CONSUMED"
          : null;
    if (eventType === "PRODUCT_DISCARDED") {
      await emitEvent(app.db, {
        type: "PRODUCT_DISCARDED",
        payload: {
          inventoryItemId: id,
          quantity: Math.abs(actualDelta),
          unit: item.unit,
          valueMinor,
          reason: input.note ?? null,
        },
        userId: req.userId,
        householdId: item.householdId,
        correlationId: req.correlationId,
      });
    } else if (eventType === "PRODUCT_CONSUMED") {
      await emitEvent(app.db, {
        type: "PRODUCT_CONSUMED",
        payload: {
          inventoryItemId: id,
          quantity: Math.abs(actualDelta),
          unit: item.unit,
          refType: "manual",
        },
        userId: req.userId,
        householdId: item.householdId,
        correlationId: req.correlationId,
      });
    }

    return { item: updated };
  });

  app.get("/v1/inventory/items/:id/transactions", auth, async (req) => {
    const { id } = parse(idParamSchema, req.params);
    await getOwnedItem(app, id, req.userId);
    const txs = await app.db
      .select()
      .from(schema.inventoryTransactions)
      .where(eq(schema.inventoryTransactions.inventoryItemId, id))
      .orderBy(desc(schema.inventoryTransactions.createdAt))
      .limit(100);
    return { transactions: txs };
  });

  app.delete("/v1/inventory/items/:id", auth, async (req) => {
    const { id } = parse(idParamSchema, req.params);
    const item = await getOwnedItem(app, id, req.userId);
    // Radering = correction till 0 + arkivering (historiken bevaras, spec §8).
    if (item.quantity > 0) {
      await app.db.insert(schema.inventoryTransactions).values({
        householdId: item.householdId,
        inventoryItemId: id,
        type: "correction",
        quantityDelta: -item.quantity,
        unit: item.unit,
        actorUserId: req.userId,
        note: "Post borttagen av användare",
      });
    }
    await app.db
      .update(schema.inventoryItems)
      .set({ quantity: 0, depletedAt: new Date(), updatedAt: new Date() })
      .where(eq(schema.inventoryItems.id, id));
    return { ok: true };
  });
}

async function getOwnedItem(app: FastifyInstance, itemId: string, userId: string) {
  const [item] = await app.db
    .select()
    .from(schema.inventoryItems)
    .where(eq(schema.inventoryItems.id, itemId))
    .limit(1);
  if (!item) throw errors.notFound("Lagerposten finns inte.");
  await requireMembership(app.db, item.householdId, userId);
  return item;
}
