import type { FastifyInstance } from "fastify";
import { and, eq } from "drizzle-orm";
import { schema } from "@app/database";
import { diffScans, type ScanDiffRow, computeBalance } from "@app/inventory-engine";
import { z } from "zod";
import type { Unit } from "@app/shared-types";
import { errors, parse } from "../lib/errors.js";
import {
  requireActiveHousehold,
  requireMembership,
  trackProductAnalytics,
} from "../lib/helpers.js";
import { scanDiffApplyInputSchema } from "@app/validation";
import { inventoryConflictCreated, inventoryConflictResolved } from "@app/analytics";

/**
 * Scan-to-scan-diff + konfliktlösning (Fas 2 §5.5–5.6).
 *
 * - ALDRIG automatiska negativa transaktioner.
 * - "vanished" är förslag som kräver användarbekräftelse.
 * - Varje automatisk diff skapar en undo-post (samma transaktion kan ångras via negativ delta).
 */
export async function scanDiffRoutes(app: FastifyInstance) {
  const auth = { preHandler: [app.authenticate] };

  /** Jämför nuvarande lager mot ett nytt scan-resultat. */
  app.post("/v1/scans/:id/diff", auth, async (req) => {
    const params = z.object({ id: z.uuid() }).parse(req.params);
    const householdId = await requireActiveHousehold(app.db, req.userId);
    await requireMembership(app.db, householdId, req.userId);

    const [job] = await app.db
      .select()
      .from(schema.scanJobs)
      .where(eq(schema.scanJobs.id, params.id))
      .limit(1);
    if (!job || job.userId !== req.userId) throw errors.notFound("Skanningen finns inte.");
    if (!job.result) throw errors.badRequest("Skanningen har inget resultat än.");

    const observations = extractObservations(job.result as Record<string, unknown>);

    const items = await app.db
      .select()
      .from(schema.inventoryItems)
      .where(eq(schema.inventoryItems.householdId, householdId));

    const previousItems = items
      .filter((i) => i.quantity > 0 && !i.depletedAt)
      .map((i) => ({
        id: i.id,
        displayName: i.displayName,
        quantity: i.quantity,
        unit: i.unit,
        storageLocationId: i.storageLocationId,
        bestBeforeDate: i.bestBeforeDate,
        useByDate: i.useByDate,
      }));

    const diff = diffScans(previousItems, observations, new Date());

    // Identifiera osäkra differ som konflikter (för review-vyn)
    const conflicts = diff.rows
      .filter((r) => r.kind === "vanished" || r.confidence < 0.75)
      .map((row) => ({
        kind: row.kind,
        displayName: row.displayName,
        previousItemId: row.previousItemId,
        proposedResolution:
          row.kind === "vanished"
            ? "depleted"
            : { quantity: row.newQuantity, locationId: row.newLocationId },
      }));

    return {
      scanJobId: params.id,
      rows: diff.rows,
      pendingVanishedIds: diff.pendingVanishedIds,
      conflicts,
      modelVersion: job.modelVersion,
      promptVersion: job.promptVersion,
    };
  });

  /** Applicera bekräftade diff-rader. Idempotency via idempotencyKeys. */
  app.post("/v1/scans/:id/diff/apply", auth, async (req) => {
    const params = z.object({ id: z.uuid() }).parse(req.params);
    const householdId = await requireActiveHousehold(app.db, req.userId);
    await requireMembership(app.db, householdId, req.userId);

    const input = parse(scanDiffApplyInputSchema, req.body);

    // Idempotency
    if (input.idempotencyKey) {
      const [existing] = await app.db
        .select()
        .from(schema.idempotencyKeys)
        .where(
          and(
            eq(schema.idempotencyKeys.userId, req.userId),
            eq(schema.idempotencyKeys.key, input.idempotencyKey),
            eq(schema.idempotencyKeys.endpoint, "POST /v1/scans/:id/diff/apply"),
          ),
        )
        .limit(1);
      if (existing?.responseBody) {
        return existing.responseBody as { appliedItemIds: string[] };
      }
    }

    const [job] = await app.db
      .select()
      .from(schema.scanJobs)
      .where(eq(schema.scanJobs.id, params.id))
      .limit(1);
    if (!job || job.userId !== req.userId) throw errors.notFound("Skanningen finns inte.");

    const appliedItemIds: string[] = [];

    for (const row of input.rows) {
      if (row.kind === "new_item") {
        const locationId =
          row.newLocationId ?? (await defaultLocation(app, householdId, job.scanType));
        if (!locationId) continue;
        const [inv] = await app.db
          .insert(schema.inventoryItems)
          .values({
            householdId,
            displayName: row.displayName,
            quantity: row.newQuantity ?? 1,
            unit: row.unit as Unit,
            storageLocationId: locationId,
            source: scanSource(job.scanType),
            confidence: row.confidence,
            verifiedByUser: row.confidence >= 0.9,
            lastVerifiedAt: row.confidence >= 0.9 ? new Date() : null,
            modelVersion: job.modelVersion,
            promptVersion: job.promptVersion,
          })
          .returning();
        await app.db.insert(schema.inventoryTransactions).values({
          householdId,
          inventoryItemId: inv!.id,
          type: "purchase",
          quantityDelta: row.newQuantity ?? 1,
          unit: row.unit as Unit,
          refType: "scan_diff",
          refId: params.id,
          actorUserId: req.userId,
        });
        appliedItemIds.push(inv!.id);
      } else if (row.kind === "quantity_changed" && row.previousItemId) {
        const [item] = await app.db
          .select()
          .from(schema.inventoryItems)
          .where(eq(schema.inventoryItems.id, row.previousItemId))
          .limit(1);
        if (!item) continue;
        const newQty = row.newQuantity ?? item.quantity;
        const delta = newQty - item.quantity;
        await app.db
          .update(schema.inventoryItems)
          .set({ quantity: newQty, updatedAt: new Date() })
          .where(eq(schema.inventoryItems.id, item.id));
        if (Math.abs(delta) > 1e-6) {
          await app.db.insert(schema.inventoryTransactions).values({
            householdId,
            inventoryItemId: item.id,
            type: "correction",
            quantityDelta: delta,
            unit: item.unit,
            refType: "scan_diff",
            refId: params.id,
            actorUserId: req.userId,
          });
        }
        appliedItemIds.push(item.id);
      } else if (row.kind === "moved" && row.previousItemId) {
        const [item] = await app.db
          .select()
          .from(schema.inventoryItems)
          .where(eq(schema.inventoryItems.id, row.previousItemId))
          .limit(1);
        if (!item) continue;
        const newLocationId = row.newLocationId ?? item.storageLocationId;
        const newQty = row.newQuantity ?? item.quantity;
        const delta = newQty - item.quantity;
        await app.db
          .update(schema.inventoryItems)
          .set({ storageLocationId: newLocationId, quantity: newQty, updatedAt: new Date() })
          .where(eq(schema.inventoryItems.id, item.id));
        if (Math.abs(delta) > 1e-6) {
          await app.db.insert(schema.inventoryTransactions).values({
            householdId,
            inventoryItemId: item.id,
            type: "correction",
            quantityDelta: delta,
            unit: item.unit,
            refType: "scan_diff",
            refId: params.id,
            actorUserId: req.userId,
          });
        }
        appliedItemIds.push(item.id);
      } else if (row.kind === "depleted" && row.previousItemId) {
        // Användaren har explicit bekräftat "vanished" → depleted
        const [item] = await app.db
          .select()
          .from(schema.inventoryItems)
          .where(eq(schema.inventoryItems.id, row.previousItemId))
          .limit(1);
        if (!item) continue;
        await app.db
          .update(schema.inventoryItems)
          .set({ quantity: 0, depletedAt: new Date(), updatedAt: new Date() })
          .where(eq(schema.inventoryItems.id, item.id));
        await app.db.insert(schema.inventoryTransactions).values({
          householdId,
          inventoryItemId: item.id,
          type: "correction",
          quantityDelta: -item.quantity,
          unit: item.unit,
          refType: "scan_diff",
          refId: params.id,
          actorUserId: req.userId,
        });
        appliedItemIds.push(item.id);
      }
    }

    // Transaktionsinvariant: validera att saldot stämmer för alla berörda varor
    for (const itemId of appliedItemIds) {
      const [item] = await app.db
        .select({ quantity: schema.inventoryItems.quantity })
        .from(schema.inventoryItems)
        .where(eq(schema.inventoryItems.id, itemId))
        .limit(1);
      const txs = await app.db
        .select({
          type: schema.inventoryTransactions.type,
          quantityDelta: schema.inventoryTransactions.quantityDelta,
          unit: schema.inventoryTransactions.unit,
        })
        .from(schema.inventoryTransactions)
        .where(eq(schema.inventoryTransactions.inventoryItemId, itemId));
      const balance = computeBalance(txs);
      if (Math.abs(balance.balance - (item?.quantity ?? 0)) > 1e-6) {
        throw errors.internal("Transaktionsinvarianten bröts efter scan-diff.");
      }
    }

    const response = { appliedItemIds };

    if (input.idempotencyKey) {
      await app.db
        .insert(schema.idempotencyKeys)
        .values({
          userId: req.userId,
          key: input.idempotencyKey,
          endpoint: "POST /v1/scans/:id/diff/apply",
          responseStatus: 200,
          responseBody: response as Record<string, unknown>,
        })
        .onConflictDoNothing();
    }

    return response;
  });

  /** Lista öppna konflikter för hushållet. */
  app.get("/v1/inventory/conflicts", auth, async (req) => {
    const householdId = await requireActiveHousehold(app.db, req.userId);
    await requireMembership(app.db, householdId, req.userId);
    const conflicts = await app.db
      .select()
      .from(schema.inventoryConflicts)
      .where(
        and(
          eq(schema.inventoryConflicts.householdId, householdId),
          eq(schema.inventoryConflicts.status, "open"),
        ),
      )
      .orderBy(schema.inventoryConflicts.createdAt);
    return { conflicts };
  });

  /** Lös en konflikt med valfritt resultat; skriver alltid vanliga transaktioner. */
  app.post("/v1/inventory/conflicts/:id/resolve", auth, async (req) => {
    const params = z.object({ id: z.uuid() }).parse(req.params);
    const householdId = await requireActiveHousehold(app.db, req.userId);
    await requireMembership(app.db, householdId, req.userId);

    const body = z
      .object({
        resolution: z.enum(["source_a", "source_b", "manual"]),
        manualQuantity: z.number().min(0).optional(),
        manualLocationId: z.string().uuid().optional(),
        note: z.string().max(200).optional(),
      })
      .parse(req.body);

    const [conflict] = await app.db
      .select()
      .from(schema.inventoryConflicts)
      .where(eq(schema.inventoryConflicts.id, params.id))
      .limit(1);
    if (!conflict || conflict.householdId !== householdId)
      throw errors.notFound("Konflikten finns inte.");
    if (conflict.status !== "open") throw errors.badRequest("Konflikten är redan löst.");

    const resolved = resolveConflictPayload(conflict, body);

    if (resolved.quantity != null && conflict.inventoryItemId) {
      const [item] = await app.db
        .select()
        .from(schema.inventoryItems)
        .where(eq(schema.inventoryItems.id, conflict.inventoryItemId))
        .limit(1);
      if (item) {
        const delta = resolved.quantity - item.quantity;
        if (Math.abs(delta) > 1e-6 || resolved.status === "depleted") {
          const finalQty = resolved.status === "depleted" ? 0 : resolved.quantity;
          await app.db
            .update(schema.inventoryItems)
            .set({
              quantity: finalQty,
              storageLocationId: resolved.locationId ?? item.storageLocationId,
              depletedAt: resolved.status === "depleted" ? new Date() : item.depletedAt,
              updatedAt: new Date(),
            })
            .where(eq(schema.inventoryItems.id, item.id));
          await app.db.insert(schema.inventoryTransactions).values({
            householdId,
            inventoryItemId: item.id,
            type: "correction",
            quantityDelta: finalQty - item.quantity,
            unit: item.unit,
            refType: "conflict",
            refId: conflict.id,
            actorUserId: req.userId,
            note: body.note,
          });
        }
      }
    }

    await app.db
      .update(schema.inventoryConflicts)
      .set({
        status: "resolved",
        resolvedByUserId: req.userId,
        resolution: { ...resolved, note: body.note } as Record<string, unknown>,
        resolvedAt: new Date(),
      })
      .where(eq(schema.inventoryConflicts.id, params.id));

    await trackProductAnalytics(
      app.db,
      req.userId,
      inventoryConflictResolved({
        householdId,
        properties: { conflictId: conflict.id, resolution: body.resolution },
      }),
    );

    return { ok: true };
  });
}

function extractObservations(result: Record<string, unknown> | unknown[]): Array<{
  displayName: string;
  quantity: number;
  unit: string;
  storageLocationId: string;
  bestBeforeDate?: string | null;
  useByDate?: string | null;
  observationConfidence: number;
}> {
  const raw = Array.isArray(result)
    ? result
    : Array.isArray(result.items)
      ? (result.items as unknown[])
      : [];
  return raw
    .map((r) => {
      const item = r as Record<string, unknown>;
      return {
        displayName: String(item.displayName ?? ""),
        quantity: Number(item.quantity ?? 1),
        unit: String(item.unit ?? "COUNT"),
        storageLocationId: String(item.storageLocationId ?? ""),
        bestBeforeDate: item.bestBeforeDate ? String(item.bestBeforeDate) : null,
        useByDate: item.useByDate ? String(item.useByDate) : null,
        observationConfidence: Number(item.confidence ?? item.observationConfidence ?? 0.8),
      };
    })
    .filter((r) => r.displayName && r.storageLocationId);
}

function scanSource(scanType: string) {
  switch (scanType) {
    case "fridge":
      return "fridge_photo" as const;
    case "freezer":
      return "freezer_photo" as const;
    case "pantry":
    case "ingredients":
      return "pantry_photo" as const;
    default:
      return "label_photo" as const;
  }
}

async function defaultLocation(app: FastifyInstance, householdId: string, scanType: string) {
  const wanted =
    scanType === "freezer"
      ? "freezer"
      : scanType === "pantry" || scanType === "ingredients"
        ? "pantry"
        : "fridge";
  const [loc] = await app.db
    .select({ id: schema.storageLocations.id })
    .from(schema.storageLocations)
    .where(
      and(
        eq(schema.storageLocations.householdId, householdId),
        eq(schema.storageLocations.type, wanted),
      ),
    )
    .limit(1);
  return loc?.id ?? null;
}

function resolveConflictPayload(
  conflict: typeof schema.inventoryConflicts.$inferSelect,
  body: {
    resolution: "source_a" | "source_b" | "manual";
    manualQuantity?: number;
    manualLocationId?: string;
  },
): { quantity?: number; locationId?: string; status?: string } {
  const a = (conflict.payloadA ?? {}) as Record<string, unknown>;
  const b = (conflict.payloadB ?? {}) as Record<string, unknown>;
  if (body.resolution === "source_a") {
    return {
      quantity: a.quantity != null ? Number(a.quantity) : undefined,
      locationId: a.locationId ? String(a.locationId) : undefined,
      status: a.status ? String(a.status) : undefined,
    };
  }
  if (body.resolution === "source_b") {
    return {
      quantity: b.quantity != null ? Number(b.quantity) : undefined,
      locationId: b.locationId ? String(b.locationId) : undefined,
      status: b.status ? String(b.status) : undefined,
    };
  }
  return {
    quantity: body.manualQuantity,
    locationId: body.manualLocationId,
    status: body.manualQuantity === 0 ? "depleted" : "exists",
  };
}
