import type { FastifyInstance } from "fastify";
import { and, desc, eq, gt, ilike, isNull, or, sql } from "drizzle-orm";
import { schema } from "@app/database";
import {
  createInventoryItemInputSchema,
  idParamSchema,
  inventoryNaturalSearchQuerySchema,
  inventoryQuerySchema,
  inventoryTransactionInputSchema,
  updateInventoryItemInputSchema,
} from "@app/validation";
import {
  classifyExpiry,
  findDuplicateCandidates,
  normalizeDelta,
  computeTrust,
  householdTrustScore,
} from "@app/inventory-engine";
import { errors, parse } from "../lib/errors.js";
import {
  emitEvent,
  getActiveDecayProfile,
  requireActiveHousehold,
  requireMembership,
} from "../lib/helpers.js";
import { buildNaturalSearchResponse } from "../lib/inventorySearchResponse.js";
import { userLanguageTag } from "../lib/contentLanguage.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 decayProfile = await getActiveDecayProfile(app.db);

    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,
      });
      const trust = computeTrust(
        {
          confidence: r.item.confidence,
          verifiedByUser: r.item.verifiedByUser,
          lastVerifiedAt: r.item.lastVerifiedAt,
          quantity: r.item.quantity,
          updatedAt: r.item.updatedAt,
        },
        new Date(),
        decayProfile,
      );
      return {
        ...r.item,
        locationName: r.locationName,
        locationType: r.locationType,
        expiry,
        trustState: trust.state,
        trustScore: trust.score,
      };
    });

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

    const thirtyDaysAgo = new Date(Date.now() - 30 * 86_400_000);
    const txStats = await app.db
      .select({
        total: sql<number>`count(*)`,
        adjustments: sql<number>`count(*) FILTER (WHERE ${schema.inventoryTransactions.type} = 'adjust')`,
      })
      .from(schema.inventoryTransactions)
      .where(
        and(
          eq(schema.inventoryTransactions.householdId, householdId),
          sql`${schema.inventoryTransactions.createdAt} >= ${thirtyDaysAgo}`,
        ),
      );

    const householdTrust = householdTrustScore(
      {
        items: rows.map((r) => r.item),
        correctionCount30d: Number(txStats[0]?.adjustments ?? 0),
        transactionCount30d: Number(txStats[0]?.total ?? 0),
      },
      new Date(),
    );

    return {
      items: filtered,
      trustStatus: householdTrust.status,
    };
  });

  /**
   * Naturlig-språklig fritextsökning i lagret (FoodTwin + trust-hedge).
   * Returnerar både strukturerade träffar och ett mänskligt svar.
   */
  app.get("/v1/inventory/natural-search", auth, async (req) => {
    const query = parse(inventoryNaturalSearchQuerySchema, req.query);
    const householdId = await requireActiveHousehold(app.db, req.userId);
    const decayProfile = await getActiveDecayProfile(app.db);
    const languageTag = query.languageTag ?? (await userLanguageTag(app.db, req.userId));

    const q = query.q;
    const pattern = `%${q}%`;
    const similarityThreshold = 0.15;

    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(
          eq(schema.inventoryItems.householdId, householdId),
          isNull(schema.inventoryItems.depletedAt),
          gt(schema.inventoryItems.quantity, 0),
          or(
            sql`unaccent(${schema.inventoryItems.displayName}) ILIKE unaccent(${pattern})`,
            sql`similarity(${schema.inventoryItems.displayName}, ${q}) > ${similarityThreshold}`,
            sql`unaccent(${schema.canonicalIngredients.nameSv}) ILIKE unaccent(${pattern})`,
            sql`unaccent(${schema.canonicalIngredients.nameEn}) ILIKE unaccent(${pattern})`,
            sql`similarity(${schema.canonicalIngredients.nameSv}, ${q}) > ${similarityThreshold}`,
            sql`similarity(${schema.canonicalIngredients.nameEn}, ${q}) > ${similarityThreshold}`,
            sql`unaccent(${schema.inventoryItems.brand}) ILIKE unaccent(${pattern})`,
          )!,
        ),
      )
      .orderBy(
        desc(
          sql`GREATEST(
            similarity(${schema.inventoryItems.displayName}, ${q}),
            similarity(${schema.canonicalIngredients.nameSv}, ${q}),
            similarity(${schema.canonicalIngredients.nameEn}, ${q})
          )`,
        ),
      )
      .limit(query.limit);

    const resultItems = 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,
      });
      const trust = computeTrust(
        {
          confidence: r.item.confidence,
          verifiedByUser: r.item.verifiedByUser,
          lastVerifiedAt: r.item.lastVerifiedAt,
          quantity: r.item.quantity,
          updatedAt: r.item.updatedAt,
        },
        new Date(),
        decayProfile,
      );
      return {
        ...r.item,
        locationName: r.locationName,
        locationType: r.locationType,
        expiry,
        trustState: trust.state,
        trustScore: trust.score,
      };
    });

    const naturalItems = resultItems.map((item) => ({
      displayName: item.displayName,
      quantity: item.quantity,
      unit: item.unit,
      locationName: item.locationName,
      locationType: item.locationType,
      sublocation: item.sublocation,
      trustState: item.trustState,
      trustScore: item.trustScore,
    }));

    const response = buildNaturalSearchResponse(naturalItems, languageTag);

    return {
      query: q,
      response,
      items: resultItems,
    };
  });

  /** 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 decayProfile = await getActiveDecayProfile(app.db);
    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) => {
        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,
        });
        const trust = computeTrust(
          {
            confidence: r.item.confidence,
            verifiedByUser: r.item.verifiedByUser,
            lastVerifiedAt: r.item.lastVerifiedAt,
            quantity: r.item.quantity,
            updatedAt: r.item.updatedAt,
          },
          new Date(),
          decayProfile,
        );
        return {
          ...r.item,
          expiry,
          trustState: trust.state,
          trustScore: trust.score,
        };
      })
      .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 now = new Date();
    const confidence = input.source === "manual_search" || input.source === "free_text" ? 1 : 0.9;
    const trustState = computeTrust({
      confidence,
      verifiedByUser: true,
      lastVerifiedAt: now,
      quantity: input.quantity,
      updatedAt: now,
    }).state;

    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,
        verifiedByUser: true,
        lastVerifiedAt: now,
        trustState,
      })
      .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 trustState = computeTrust({
      confidence: updates.confidence as number | undefined ?? item.confidence,
      verifiedByUser: (updates.verifiedByUser as boolean | undefined) ?? item.verifiedByUser,
      lastVerifiedAt: (updates.lastVerifiedAt as Date | null | undefined) ?? item.lastVerifiedAt,
      quantity: (updates.quantity as number | undefined) ?? item.quantity,
      updatedAt: updates.updatedAt as Date,
    }).state;
    updates.trustState = trustState;

    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 updatedAt = new Date();
    const trustState = computeTrust({
      confidence: item.confidence,
      verifiedByUser: item.verifiedByUser,
      lastVerifiedAt: item.lastVerifiedAt,
      quantity: newQuantity,
      updatedAt,
    }).state;

    const [updated] = await app.db
      .update(schema.inventoryItems)
      .set({
        quantity: newQuantity,
        depletedAt: newQuantity <= 0 ? new Date() : null,
        trustState,
        updatedAt,
      })
      .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;
}
