import type { FastifyInstance } from "fastify";
import { and, eq, gt, isNull } from "drizzle-orm";
import { schema } from "@app/database";
import type { JobType, ScanType } from "@app/shared-types";
import { confirmScanInputSchema, createScanInputSchema, idParamSchema } from "@app/validation";
import { errors, parse } from "../lib/errors.js";
import { emitEvent, requireActiveHousehold } from "../lib/helpers.js";
import { consumeAiScan } from "../lib/entitlements.js";

/**
 * Skanningsflödet (spec §50):
 *   App → POST /v1/scans (kvotkontroll + presignade upload-URL:er)
 *       → PUT bild(er) till storage
 *       → POST /v1/scans/:id/start (läggs på kö → worker → AAMOS)
 *       → GET  /v1/scans/:id (poll: status + resultat)
 *       → POST /v1/scans/:id/confirm (användaren godkänner → lagret uppdateras)
 *
 * Användarbekräftelse är obligatorisk innan något skrivs till Food Twin
 * (spec §10, §61.5). Korrigeringar sparas som ai_corrections (spec §33).
 */

const SCAN_TO_JOB: Record<ScanType, JobType> = {
  fridge: "ANALYZE_FRIDGE_IMAGE",
  freezer: "ANALYZE_FRIDGE_IMAGE",
  pantry: "ANALYZE_PANTRY_IMAGE",
  ingredients: "ANALYZE_PANTRY_IMAGE",
  plate: "ANALYZE_MEAL_IMAGE",
  receipt: "READ_RECEIPT",
  barcode: "NORMALIZE_PRODUCTS",
  expiry_date: "READ_EXPIRY_DATE",
  nutrition_label: "READ_NUTRITION_LABEL",
  product_package: "READ_NUTRITION_LABEL",
};

const S3_PREFIX: Partial<Record<ScanType, string>> = {
  fridge: "fridge-scans",
  freezer: "fridge-scans",
  pantry: "pantry-scans",
  ingredients: "pantry-scans",
  plate: "meal-scans",
  receipt: "receipts",
  expiry_date: "product-images",
  nutrition_label: "product-images",
  product_package: "product-images",
};

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

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

    // Streckkod är gratis uppslag utan AI – hanteras direkt (spec §11: lokalt + databas).
    if (input.scanType === "barcode") {
      if (!input.barcode) throw errors.badRequest("barcode krävs för streckkodsskanning.");
      const product = await lookupBarcode(app, input.barcode);
      const [job] = await app.db
        .insert(schema.scanJobs)
        .values({
          userId: req.userId,
          householdId,
          scanType: "barcode",
          jobType: "NORMALIZE_PRODUCTS",
          status: product ? "completed" : "failed",
          result: product ? { product } : null,
          error: product
            ? null
            : "Produkten hittades inte. Fota framsida + näringsdeklaration så lägger vi till den.",
          completedAt: new Date(),
        })
        .returning();
      return reply.status(201).send({ scan: job, product });
    }

    // AI-skanning: kvotkontroll (fair use, spec §45–46) och presignade URL:er.
    await consumeAiScan(app.db, req.userId);

    const prefix = `${S3_PREFIX[input.scanType] ?? "temporary"}/${householdId}`;
    const uploads = [];
    for (let i = 0; i < Math.max(1, input.imageCount); i++) {
      uploads.push(await app.storage.presignUpload(prefix, input.contentType));
    }

    const [job] = await app.db
      .insert(schema.scanJobs)
      .values({
        userId: req.userId,
        householdId,
        scanType: input.scanType,
        jobType: SCAN_TO_JOB[input.scanType],
        status: "queued",
        s3Keys: uploads.map((u) => u.key),
        context: input.context ?? null,
      })
      .returning();

    return reply.status(201).send({ scan: job, uploads });
  });

  app.post("/v1/scans/:id/start", auth, async (req) => {
    const { id } = parse(idParamSchema, req.params);
    const job = await getOwnedScan(app, id, req.userId);
    if (job.status !== "queued") throw errors.conflict(`Jobbet är redan ${job.status}.`);

    await app.jobQueue.add(job.jobType, {
      scanJobId: job.id,
      jobType: job.jobType,
      correlationId: req.correlationId,
    });
    return { ok: true, status: "queued" };
  });

  app.get("/v1/scans/:id", auth, async (req) => {
    const { id } = parse(idParamSchema, req.params);
    return getOwnedScan(app, id, req.userId);
  });

  app.post("/v1/scans/:id/confirm", auth, async (req) => {
    const { id } = parse(idParamSchema, req.params);
    const input = parse(confirmScanInputSchema, req.body);
    const job = await getOwnedScan(app, id, req.userId);
    if (job.status !== "awaiting_confirmation" && job.status !== "completed") {
      throw errors.conflict("Jobbet har inget resultat att bekräfta ännu.");
    }
    const householdId = job.householdId ?? (await requireActiveHousehold(app.db, req.userId));

    const fallbackLocation =
      input.storageLocationId ?? (await defaultLocation(app, householdId, job.scanType));

    const created: string[] = [];
    for (const item of input.items) {
      if (item.action === "reject") {
        await recordCorrection(app, job, item.tempId ?? null, { action: "reject" });
        continue;
      }
      if (item.action === "edit" || item.action === "add") {
        await recordCorrection(app, job, item.tempId ?? null, {
          action: item.action,
          corrected: { name: item.displayName, quantity: item.quantity, unit: item.unit },
        });
      }
      const locationId = item.storageLocationId ?? fallbackLocation;
      if (!locationId)
        throw errors.badRequest("storageLocationId saknas och ingen standardplats finns.");

      const [inv] = await app.db
        .insert(schema.inventoryItems)
        .values({
          householdId,
          canonicalIngredientId: item.canonicalIngredientId ?? null,
          displayName: item.displayName,
          brand: item.brand ?? null,
          quantity: item.quantity,
          unit: item.unit,
          storageLocationId: locationId,
          sublocation: item.sublocation ?? null,
          bestBeforeDate: item.bestBeforeDate ?? null,
          useByDate: item.useByDate ?? null,
          priceMinor: item.priceMinor ?? null,
          purchasedAt: new Date().toISOString().slice(0, 10),
          source: scanSource(job.scanType),
          confidence: item.action === "accept" ? 0.9 : 1,
          verifiedByUser: true,
          lastVerifiedAt: new Date(),
          modelVersion: job.modelVersion,
          promptVersion: job.promptVersion,
        })
        .returning();

      await app.db.insert(schema.inventoryTransactions).values({
        householdId,
        inventoryItemId: inv!.id,
        type: "purchase",
        quantityDelta: item.quantity,
        unit: item.unit,
        refType: "scan",
        refId: job.id,
        actorUserId: req.userId,
        valueMinor: item.priceMinor ?? null,
      });
      await emitEvent(app.db, {
        type: "PRODUCT_ADDED",
        payload: {
          inventoryItemId: inv!.id,
          canonicalIngredientId: item.canonicalIngredientId ?? null,
          quantity: item.quantity,
          unit: item.unit,
          source: scanSource(job.scanType),
        },
        userId: req.userId,
        householdId,
        correlationId: req.correlationId,
      });
      created.push(inv!.id);
    }

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

    return { ok: true, createdItemIds: created };
  });

  app.get("/v1/scans", auth, async (req) => {
    const jobs = await app.db
      .select()
      .from(schema.scanJobs)
      .where(eq(schema.scanJobs.userId, req.userId))
      .orderBy((await import("drizzle-orm")).desc(schema.scanJobs.createdAt))
      .limit(30);
    return { scans: jobs };
  });
}

async function getOwnedScan(app: FastifyInstance, id: string, userId: string) {
  const [job] = await app.db
    .select()
    .from(schema.scanJobs)
    .where(eq(schema.scanJobs.id, id))
    .limit(1);
  if (!job || job.userId !== userId) throw errors.notFound("Skanningen finns inte.");
  return job;
}

async function lookupBarcode(app: FastifyInstance, gtin: string) {
  // 1. Egen produktdatabas (aktuell version)
  const [own] = await app.db
    .select()
    .from(schema.products)
    .where(and(eq(schema.products.gtin, gtin), isNull(schema.products.validTo)))
    .limit(1);
  if (own) return own;

  // 2. Open Food Facts (laglig öppen källa, spec §11)
  const off = app.connectors.get("open-food-facts");
  if (off && "lookupBarcode" in off) {
    try {
      const result = await (off as { lookupBarcode(g: string): Promise<unknown> }).lookupBarcode(
        gtin,
      );
      if (result && typeof result === "object") {
        const p = result as {
          gtin: string;
          name?: string;
          brand?: string;
          ingredientsText?: string;
          nutrimentsPer100g: Record<string, number | undefined>;
          imageUrl?: string;
        };
        if (!p.name) return null;
        const n = p.nutrimentsPer100g;
        const [saved] = await app.db
          .insert(schema.products)
          .values({
            gtin: p.gtin,
            name: p.name,
            brand: p.brand ?? null,
            ingredientsText: p.ingredientsText ?? null,
            nutrition:
              n.kcal != null
                ? {
                    basis: "per_100_g",
                    values: {
                      kcal: n.kcal ?? 0,
                      proteinG: n.proteinG ?? 0,
                      carbsG: n.carbsG ?? 0,
                      fatG: n.fatG ?? 0,
                      saturatedFatG: n.saturatedFatG ?? 0,
                      fiberG: n.fiberG ?? 0,
                      sugarG: n.sugarG ?? 0,
                      saltG: n.saltG ?? 0,
                    },
                  }
                : null,
            imageUrls: p.imageUrl ? [p.imageUrl] : [],
            dataSource: "open_food_facts",
            verificationStatus: "unverified",
          })
          .onConflictDoNothing()
          .returning();
        return saved ?? null;
      }
    } catch (err) {
      app.log.warn({ err, gtin }, "OFF-uppslag misslyckades");
    }
  }
  return null;
}

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

async function defaultLocation(app: FastifyInstance, householdId: string, scanType: ScanType) {
  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;
}

async function recordCorrection(
  app: FastifyInstance,
  job: {
    id: string;
    userId: string;
    jobType: string;
    result: unknown;
    modelVersion: string | null;
    promptVersion: string | null;
  },
  tempId: string | null,
  correction: Record<string, unknown>,
) {
  const consents = await app.db
    .select()
    .from(schema.userConsents)
    .where(eq(schema.userConsents.userId, job.userId));
  const snapshot = Object.fromEntries(consents.map((c) => [c.kind, c.status]));
  await app.db.insert(schema.aiCorrections).values({
    scanJobId: job.id,
    userId: job.userId,
    taskType: job.jobType,
    aiOutput: { tempId, raw: job.result },
    userCorrection: correction,
    modelVersion: job.modelVersion,
    promptVersion: job.promptVersion,
    consentSnapshot: snapshot,
  });
  await emitEvent(app.db, {
    type: "AI_CORRECTED",
    payload: {
      scanJobId: job.id,
      taskType: job.jobType,
      field: String(correction.action ?? "unknown"),
    },
    userId: job.userId,
  });
}
