import {
  BadRequestException,
  Injectable,
  Logger,
  ServiceUnavailableException,
} from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { ParsedReceiptItem } from './dto/parsed-receipt-item.dto';
import { SaveReceiptDto } from './dto/save-receipt.dto';
import { SaveReceiptResponse } from './dto/save-receipt.response';
import { AiService, CategorySuggestion } from '../ai/ai.service';
import { CategoriesService } from '../categories/categories.service';
import { normalizeName } from '../common/utils/normalize-name';
import {
  isIgnoredReceiptAliasName,
  normalizeReceiptAliasName,
  validateReceiptAliasName,
} from '../common/utils/receipt-alias';
import { FlyerSelectionService } from '../flyer-selection/flyer-selection.service';

const IMPORTER_SERVICE_URL =
  process.env.IMPORTER_SERVICE_URL || 'http://importer-api:3001';

const RECEIPT_IMPORT_MODEL = 'importer-receipt-ai';

const WEAK_DESCRIPTORS = new Set([
  'rokt',
  'rökt',
  'kokt',
  'grillad',
  'stekt',
  'skivad',
  'strimlad',
  'fryst',
  'farsk',
  'färsk',
]);

function tokenize(value: string): string[] {
  return value
    .toLowerCase()
    .split(/[^a-z0-9åäö]+/)
    .filter((w) => w.length >= 3);
}

export function isIgnoredReceiptName(value: string | null | undefined): boolean {
  return isIgnoredReceiptAliasName(value);
}

function normalizeToken(s: string): string {
  return s.replace(/å/g, 'a').replace(/ä/g, 'a').replace(/ö/g, 'o').replace(/é/g, 'e').replace(/è/g, 'e');
}

function normalizeForRules(value: string): string {
  return value
    .toLowerCase()
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '')
    .replace(/[^a-z0-9]+/g, ' ')
    .trim();
}

function hasPorkLikeSignal(normalized: string): boolean {
  return (
    normalized.includes('bacon') ||
    normalized.includes('bacn') ||
    normalized.includes('baco') ||
    /\bbac[a-z0-9]{1,5}\b/.test(normalized) ||
    normalized.includes('sidflask') ||
    normalized.includes('pancetta') ||
    normalized.includes('flask') ||
    normalized.includes('karre') ||
    normalized.includes('kotlett')
  );
}

function hasBreadLikeSignal(normalized: string): boolean {
  return (
    /\brostbrod\b/.test(normalized) ||
    /\brost\s*n\s*toast\b/.test(normalized) ||
    /\broast\s*n\s*toast\b/.test(normalized) ||
    /\btoastbrod\b/.test(normalized) ||
    /\bformbrod\b/.test(normalized) ||
    /\blantbrod\b/.test(normalized) ||
    /\bfullkornsbrod\b/.test(normalized) ||
    /\bfranska\b/.test(normalized) ||
    /\blimpa\b/.test(normalized) ||
    /\bbrod\b/.test(normalized) ||
    /\btoast\b/.test(normalized)
  );
}

type UnitMappingLite = {
  productId: number;
  originalUnit: string;
  preferredUnit: string;
};

type ProductLite = {
  id: number;
  name: string;
  canonicalName: string | null;
  categoryRef: { id: number; name: string } | null;
};

type AliasLite = {
  receiptName: string;
  product: ProductLite;
};

type MatchingContext = {
  aliases: AliasLite[];
  aliasByReceiptName?: Map<string, AliasLite>;
  products: ProductLite[];
  unitMappings: UnitMappingLite[];
  unitMappingByKey?: Map<string, string>;
  categories: Awaited<ReturnType<CategoriesService['findFlattened']>>;
  aiEnabled: boolean;
};

type MatchDebug = {
  steps: string[];
  tree: Record<string, unknown>;
};

@Injectable()
export class ReceiptImportService {
  private readonly logger = new Logger(ReceiptImportService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly aiService: AiService,
    private readonly categoriesService: CategoriesService,
    private readonly flyerSelectionService: FlyerSelectionService,
  ) {}

  async parseReceipt(file: Express.Multer.File, _isPremium = false, userId?: number): Promise<ParsedReceiptItem[]> {
    const parseStartedAt = Date.now();
    let parseError: string | null = null;
    let tracePrompt: string | null = null;
    let traceRawOutput: string | null = null;
    let traceNormalizedOutput: Record<string, unknown> | null = null;

    // Steg 1: Delegera AI-parsning till microservice-importer
    let rawItems: ParsedReceiptItem[];
    try {
      const importer = await this.parseReceiptViaImporter(file);
      rawItems = importer.items;
      tracePrompt = importer.trace.prompt;
      traceRawOutput = importer.trace.rawOutput;
      traceNormalizedOutput = importer.trace.normalizedOutput;
    } catch (err) {
      parseError = err instanceof Error ? err.message : String(err);
      await this.persistReceiptTrace({
        userId,
        model: RECEIPT_IMPORT_MODEL,
        prompt: tracePrompt,
        rawOutput: traceRawOutput,
        normalizedOutput: traceNormalizedOutput,
        status: 'error',
        error: parseError,
        durationMs: Date.now() - parseStartedAt,
      });
      throw err;
    }

    // Steg 2 & 3: Unified matching + categorization
    // Samla context en gång för alla items
    const context = await this.prepareMatchingContext(userId);

    // Mappa alla items genom unified matcher
    const parsedItems = await Promise.all(
      rawItems.map((item) => this.matchAndEnrichReceiptItem(item, context)),
    );

    await this.persistReceiptTrace({
      userId,
      model: RECEIPT_IMPORT_MODEL,
      prompt: tracePrompt,
      rawOutput: traceRawOutput,
      normalizedOutput: {
        importer: traceNormalizedOutput,
        enrichedItems: parsedItems,
      },
      status: parsedItems.length == 0 ? 'error' : 'success',
      error: parsedItems.length == 0
          ? 'Inga kvittorader kunde tolkas av importer-tjänsten.'
          : null,
      durationMs: Date.now() - parseStartedAt,
    });

    return parsedItems;
  }

  private async prepareMatchingContext(userId?: number): Promise<MatchingContext> {
    const prismaAny = this.prisma as any;

    const productFilter = userId ? { isActive: true, ownerId: userId } : { isActive: true };
    const aliasFilter = userId
      ? { OR: [{ ownerId: userId, isGlobal: false }, { isGlobal: true }] }
      : { isGlobal: true };

    const unitMappingsPromise =
      userId && prismaAny.unitMapping?.findMany
        ? (prismaAny.unitMapping.findMany({
            where: { userId },
            select: { productId: true, originalUnit: true, preferredUnit: true },
          }) as Promise<UnitMappingLite[]>)
        : Promise.resolve([] as UnitMappingLite[]);

    let categories: Awaited<ReturnType<CategoriesService['findFlattened']>>;
    try {
      categories = await this.categoriesService.findFlattened();
    } catch (err) {
      this.logger.warn(
        `prepareMatchingContext: kunde inte ladda kategorier (${String(err)}). Kategori-förslag kan utebli.`,
      );
      categories = [];
    }
    if (categories.length === 0) {
      this.logger.warn(
        'prepareMatchingContext: inga kategorier laddade. Regel-baserade kategori-förslag blir tomma.',
      );
    }

    const [aliases, products, unitMappings] = await Promise.all([
      this.prisma.receiptAlias.findMany({
        where: aliasFilter,
        orderBy: [{ isGlobal: 'asc' }, { id: 'asc' }],
        select: {
          receiptName: true,
          product: {
            select: {
              id: true,
              name: true,
              canonicalName: true,
              categoryRef: { select: { id: true, name: true } },
            },
          },
        },
      }),
      this.prisma.product.findMany({
        where: productFilter,
        select: {
          id: true,
          name: true,
          canonicalName: true,
          categoryRef: { select: { id: true, name: true } },
        },
      }),
      unitMappingsPromise,
    ]) as [AliasLite[], ProductLite[], UnitMappingLite[]];

    const user = userId
      ? await this.prisma.user.findUnique({ where: { id: userId }, select: { aiEngineEnabled: true } })
      : null;

    const aliasByReceiptName = new Map<string, AliasLite>();
    for (const alias of aliases) {
      const normalizedReceiptName = normalizeReceiptAliasName(alias.receiptName);
      if (!normalizedReceiptName) continue;
      if (!aliasByReceiptName.has(normalizedReceiptName)) {
        aliasByReceiptName.set(normalizedReceiptName, alias);
      }
    }

    const unitMappingByKey = new Map<string, string>();
    for (const unitMapping of unitMappings) {
      const key = `${unitMapping.productId}:${unitMapping.originalUnit}`;
      if (!unitMappingByKey.has(key)) {
        unitMappingByKey.set(key, unitMapping.preferredUnit);
      }
    }

    return {
      aliases,
      aliasByReceiptName,
      products,
      unitMappings,
      unitMappingByKey,
      categories,
      aiEnabled: user?.aiEngineEnabled ?? false,
    };
  }

  private resolvePreferredUnit(
    productId: number,
    itemUnit: string | null | undefined,
    context: Pick<MatchingContext, 'unitMappings' | 'unitMappingByKey'>,
  ): string | null | undefined {
    const normalizedUnit = (itemUnit ?? '').trim().toLowerCase();
    return (
      context.unitMappingByKey?.get(`${productId}:${normalizedUnit}`) ??
      context.unitMappings.find(
        (um) => um.productId === productId && um.originalUnit === normalizedUnit,
      )?.preferredUnit
    );
  }

  private buildSignalText(item: ParsedReceiptItem): string {
    return [item.rawName, item.matchedProductName, item.suggestedProductName]
      .filter((v): v is string => typeof v === 'string' && v.trim().length > 0)
      .join(' ');
  }

  async upsertUnitMapping(
    userId: number,
    productId: number,
    originalUnit: string,
    preferredUnit: string,
  ) {
    const prismaAny = this.prisma as any;
    const normalizedOriginalUnit = originalUnit.trim().toLowerCase();
    const normalizedPreferredUnit = preferredUnit.trim().toLowerCase();

    if (!normalizedOriginalUnit || !normalizedPreferredUnit) {
      throw new BadRequestException('Enheter måste vara ifyllda.');
    }

    // Ingen inlärning behövs om enheten redan är samma.
    if (normalizedOriginalUnit === normalizedPreferredUnit) {
      return { skipped: true };
    }

    return prismaAny.unitMapping.upsert({
      where: {
        productId_originalUnit_userId: {
          productId,
          originalUnit: normalizedOriginalUnit,
          userId,
        },
      },
      update: {
        preferredUnit: normalizedPreferredUnit,
      },
      create: {
        productId,
        userId,
        originalUnit: normalizedOriginalUnit,
        preferredUnit: normalizedPreferredUnit,
      },
    });
  }

  async saveReceipt(userId: number, dto: SaveReceiptDto): Promise<SaveReceiptResponse> {
    const response: SaveReceiptResponse = {
      created: 0,
      merged: 0,
      pantryAdded: 0,
      pantrySkipped: 0,
      aliasesLearned: 0,
      unitMappingsLearned: 0,
      errors: [],
    };

    const prismaAny = this.prisma as any;
    const successfulRows: Array<{
      rawName: string;
      productId?: number;
      quantity?: number;
      unit?: string;
    }> = [];

    // Preload existierande pantry-poster för denna användare
    const userPantry = await this.prisma.pantryItem.findMany({
      where: { userId },
      select: { productId: true },
    });
    const pantryProductIds = new Set(userPantry.map((p) => p.productId));

    // Preload existierande inventarioposter för denna användare (grupperat efter productId)
    const userInventory = await this.prisma.inventoryItem.findMany({
      where: { userId },
      select: { id: true, productId: true, quantity: true, unit: true },
    });
    const inventoryByProductId = new Map<number, typeof userInventory[0]>();
    for (const item of userInventory) {
      if (!inventoryByProductId.has(item.productId)) {
        inventoryByProductId.set(item.productId, item);
      }
    }

    // Kör allt i en transaktion för atomicitet
    try {
      await this.prisma.$transaction(async (tx) => {
        const txAny = tx as any;

        for (let index = 0; index < dto.items.length; index++) {
          const item = dto.items[index];

          try {
            // === Steg 1: Bestäm/skapa produkten ===
            let productId: number;

            if (item.createProductName) {
              // Skapa ny privat produkt
              const name = item.createProductName.trim();
              const normalizedName = `private:${userId}:${normalizeName(name)}`;

              const existing = await tx.product.findUnique({
                where: { normalizedName },
              });

              if (existing && existing.isActive) {
                productId = existing.id;
              } else if (existing) {
                const updated = await tx.product.update({
                  where: { id: existing.id },
                  data: { isActive: true, deletedAt: null, name, canonicalName: name },
                });
                productId = updated.id;
              } else {
                const created = await tx.product.create({
                  data: {
                    name,
                    normalizedName,
                    canonicalName: name,
                    isActive: true,
                    isPrivate: true,
                    ownerId: userId,
                    ...(item.categoryId != null ? { categoryId: item.categoryId } : {}),
                  },
                });
                productId = created.id;
              }
            } else if (item.productId) {
              // Använd befintlig produkt
              const product = await tx.product.findUnique({
                where: { id: item.productId },
              });
              if (!product) {
                throw new Error(`Produkten med ID ${item.productId} hittades inte.`);
              }

              if (item.categoryId != null && product.categoryId !== item.categoryId) {
                await tx.product.update({
                  where: { id: product.id },
                  data: { categoryId: item.categoryId },
                });
              }

              productId = product.id;
            } else {
              throw new Error('Antingen productId eller createProductName måste anges.');
            }

            // === Steg 2: Hantera pantry eller inventory ===
            if (item.destination === 'pantry') {
              if (pantryProductIds.has(productId)) {
                response.pantrySkipped++;
              } else {
                await tx.pantryItem.create({
                  data: { userId, productId },
                });
                response.pantryAdded++;
                pantryProductIds.add(productId);
              }
            } else {
              // inventory
              const quantity = item.quantity ?? 0;
              const unit = (item.unit ?? '').trim() || 'st';

              const existing = inventoryByProductId.get(productId);
              if (existing) {
                // Slå samman
                await tx.inventoryItem.update({
                  where: { id: existing.id },
                  data: {
                    quantity: {
                      increment: new Prisma.Decimal(quantity),
                    },
                  },
                });
                response.merged++;
              } else {
                // Skapa ny
                await tx.inventoryItem.create({
                  data: {
                    userId,
                    productId,
                    quantity: new Prisma.Decimal(quantity),
                    unit,
                    brand: item.brand ?? undefined,
                    origin: item.origin ?? undefined,
                    receiptName: item.rawName,
                  },
                });
                response.created++;
                // Uppdatera local cache
                inventoryByProductId.set(productId, {
                  id: -1,
                  productId,
                  quantity: new Prisma.Decimal(quantity),
                  unit,
                });
              }

              // === Steg 3: Lär in enhetsmappning om requested ===
              if (item.learnUnitMapping) {
                const originalUnit = (item.rawName ?? '').trim().toLowerCase();
                const preferredUnit = unit.toLowerCase();
                if (originalUnit && preferredUnit && originalUnit !== preferredUnit) {
                  await txAny.unitMapping.upsert({
                    where: {
                      productId_originalUnit_userId: {
                        productId,
                        originalUnit,
                        userId,
                      },
                    },
                    update: {
                      preferredUnit,
                    },
                    create: {
                      productId,
                      userId,
                      originalUnit,
                      preferredUnit,
                    },
                  });
                  response.unitMappingsLearned++;
                }
              }
            }

            successfulRows.push({
              rawName: item.rawName,
              productId,
              quantity: item.quantity,
              unit: item.unit,
            });

            // === Steg 4: Lär in alias om requested ===
            if (item.learnAlias) {
              const normalizedReceiptName = normalizeReceiptAliasName(item.rawName);
              const aliasValidationError = validateReceiptAliasName(normalizedReceiptName);
              if (aliasValidationError) {
                throw new Error(aliasValidationError);
              }
              if (normalizedReceiptName) {
                const isGlobalAlias = item.learnAliasGlobally === true;
                const aliasOwnerId: number | null = isGlobalAlias ? null : userId || null;
                await tx.receiptAlias.upsert({
                  where: {
                    receiptName_ownerId_isGlobal: {
                      receiptName: normalizedReceiptName,
                      ownerId: aliasOwnerId as any,
                      isGlobal: isGlobalAlias,
                    },
                  },
                  update: {
                    productId,
                  },
                  create: {
                    receiptName: normalizedReceiptName,
                    productId,
                    ownerId: aliasOwnerId as any,
                    isGlobal: isGlobalAlias,
                  },
                });
                response.aliasesLearned++;
              }
            }
          } catch (err) {
            const errorMsg = err instanceof Error ? err.message : String(err);
            this.logger.warn(
              `saveReceipt item [${index}] error: ${errorMsg}`,
            );
            response.errors = response.errors ?? [];
            response.errors.push({ index, error: errorMsg });
          }
        }
      });
    } catch (err) {
      this.logger.error(`saveReceipt transaction failed: ${err}`);
      throw new BadRequestException(
        `Transaktionfel vid sparande av kvittovaror: ${err instanceof Error ? err.message : String(err)}`,
      );
    }

    if (successfulRows.length > 0) {
      const syncPayload = {
        items: successfulRows,
        receiptImportBatchId: `receipt-save-${Date.now()}-${userId}`,
        boughtSource: 'receipt_auto' as const,
      };
      try {
        const sync = await this.flyerSelectionService.commitReceiptMatches(userId, syncPayload);
        response.flyerAutoSync = {
          bought: sync.boughtCount,
          ambiguous: sync.ambiguousCount,
          unmatched: sync.unmatchedCount,
        };
      } catch (err) {
        this.logger.warn(`Flyer auto-sync failed after receipt save (attempt 1): ${String(err)}`);
        try {
          const sync = await this.flyerSelectionService.commitReceiptMatches(userId, syncPayload);
          response.flyerAutoSync = {
            bought: sync.boughtCount,
            ambiguous: sync.ambiguousCount,
            unmatched: sync.unmatchedCount,
          };
        } catch (retryErr) {
          const message = retryErr instanceof Error ? retryErr.message : String(retryErr);
          this.logger.warn(`Flyer auto-sync failed after receipt save (attempt 2): ${message}`);
          response.flyerAutoSync = {
            bought: 0,
            ambiguous: 0,
            unmatched: 0,
            error: message,
          };
        }
      }
    }

    return response;
  }

  private async parseReceiptViaImporter(file: Express.Multer.File): Promise<{
    items: ParsedReceiptItem[];
    trace: {
      prompt: string | null;
      rawOutput: string | null;
      normalizedOutput: Record<string, unknown> | null;
    };
  }> {
    const form = new FormData();
    form.append(
      'file',
      new Blob([new Uint8Array(file.buffer)], { type: file.mimetype }),
      file.originalname,
    );

    let response: Response;
    try {
      response = await fetch(`${IMPORTER_SERVICE_URL}/api/receipt-import/parse`, {
        method: 'POST',
        body: form,
      });
    } catch (err) {
      this.logger.error(`Kunde inte nå importer-api för kvittoparsning: ${err}`);
      throw new ServiceUnavailableException(
        'Import-tjänsten är inte tillgänglig. Försök igen senare.',
      );
    }

    if (!response.ok) {
      let message = `Importer svarade ${response.status}`;
      try {
        const body = (await response.json()) as { message?: string };
        if (body.message) message = body.message;
      } catch {
        // ignorera parse-fel
      }
      if (response.status === 503 || response.status === 429) {
        throw new ServiceUnavailableException(message);
      }
      throw new BadRequestException(message);
    }

    const body = (await response.json()) as
      | ParsedReceiptItem[]
      | {
          items?: ParsedReceiptItem[];
          prompt?: unknown;
          rawOutput?: unknown;
          normalizedOutput?: Record<string, unknown>;
        };

    const normalizedItems = this.extractImporterItems(body)
      .filter((item) => !isIgnoredReceiptName(item.rawName));

    return {
      items: normalizedItems,
      trace: {
        prompt: this.extractImporterPrompt(body),
        rawOutput: this.extractImporterRawOutput(body),
        normalizedOutput: this.extractImporterNormalizedOutput(body),
      },
    };
  }

  private extractImporterItems(
    body: ParsedReceiptItem[] | { items?: ParsedReceiptItem[] },
  ): ParsedReceiptItem[] {
    if (Array.isArray(body)) return body;
    if (Array.isArray(body.items)) return body.items;
    return [];
  }

  private extractImporterPrompt(
    body: ParsedReceiptItem[] | { prompt?: unknown },
  ): string | null {
    if (Array.isArray(body)) return null;
    if (typeof body.prompt !== 'string') return null;
    const prompt = body.prompt.trim();
    return prompt && prompt.length > 0 ? prompt : null;
  }

  private extractImporterRawOutput(
    body: ParsedReceiptItem[] | { rawOutput?: unknown },
  ): string | null {
    if (Array.isArray(body)) return JSON.stringify(body);
    if (typeof body.rawOutput === 'string' && body.rawOutput.trim().length > 0) {
      return body.rawOutput;
    }
    if (body.rawOutput !== undefined) {
      try {
        return JSON.stringify(body.rawOutput);
      } catch {
        return String(body.rawOutput);
      }
    }
    return JSON.stringify(body);
  }

  private extractImporterNormalizedOutput(
    body: ParsedReceiptItem[] | { normalizedOutput?: Record<string, unknown>; items?: ParsedReceiptItem[] },
  ): Record<string, unknown> | null {
    if (Array.isArray(body)) {
      return { items: body };
    }
    if (body.normalizedOutput && typeof body.normalizedOutput === 'object') {
      return body.normalizedOutput;
    }
    if (Array.isArray(body.items)) {
      return { items: body.items };
    }
    return null;
  }

  private async persistReceiptTrace(params: {
    userId?: number;
    model: string;
    prompt: string | null;
    rawOutput: string | null;
    normalizedOutput: Record<string, unknown> | null;
    status: 'success' | 'error';
    error: string | null;
    durationMs: number;
  }): Promise<void> {
    try {
      await this.prisma.aiTrace.create({
        data: {
          source: 'receipt',
          userId: params.userId,
          model: params.model,
          prompt: params.prompt,
          rawOutput: params.rawOutput,
          ...(params.normalizedOutput == null
              ? {}
              : {
                  normalizedOutput:
                      params.normalizedOutput as Prisma.InputJsonValue,
                }),
          status: params.status,
          error: params.error,
          durationMs: params.durationMs,
        },
      });
    } catch (traceErr) {
      this.logger.warn(
        `Kunde inte spara receipt AI-trace: ${traceErr instanceof Error ? traceErr.message : String(traceErr)}`,
      );
    }
  }

  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  // UNIFIED MATCHER: Kombinerar product matching + categorization
  //
  // KATEGORI-HIERARKI (fallback-first):
  // 1. Alias-kopplad produkt (om fanns)
  // 2. Word-match-kopplad produkt (om fanns)
  // 3. Regel-baserad (deterministisk, alltid försökt)
  // 4. AI-kategorisering (BARA som fallback när allt annat misslyckades, och om aktiverat)
  //
  // AI kallas ALDRIG om regel-baserad eller produkt-koppling redan satte kategori.
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  private async matchAndEnrichReceiptItem(
    item: ParsedReceiptItem,
    context: MatchingContext,
  ): Promise<ParsedReceiptItem> {
    if (!item.rawName) return item;

    const raw = normalizeReceiptAliasName(item.rawName);
    const debug: MatchDebug = { steps: [], tree: {} };

    try {
      // ┌─ STEG 1: Alias-lookup (certifierad match) ─────────────────────────┐
      debug.steps.push('Step 1: Alias lookup');
      const aliasMatch =
        context.aliasByReceiptName?.get(raw) ??
        context.aliases.find((a) => a.receiptName === raw);
      if (aliasMatch) {
        debug.tree.alias = { found: true, productId: aliasMatch.product.id };
        debug.steps.push(`  ✓ Alias found → productId ${aliasMatch.product.id}`);

        const mappedUnit = this.resolvePreferredUnit(
          aliasMatch.product.id,
          item.unit,
          context,
        );

        const aliasResult: ParsedReceiptItem = {
          ...item,
          matchedProductId: aliasMatch.product.id,
          matchedProductName: aliasMatch.product.canonicalName ?? aliasMatch.product.name,
          unit: mappedUnit ?? item.unit,
          matchedVia: 'alias' as const,
          ...(aliasMatch.product.categoryRef
            ? {
                categorySuggestion: {
                  categoryId: aliasMatch.product.categoryRef.id,
                  categoryName: aliasMatch.product.categoryRef.name,
                  path: aliasMatch.product.categoryRef.name,
                  confidence: 'high' as const,
                  usedFallback: false,
                },
              }
            : {}),
        };

        // Kör alltid enrichCategoryForItem för guard-funktioner och hard overrides
        return await this.enrichCategoryForItem(aliasResult, context, debug);
      }
      debug.steps.push(`  ✗ No alias match`);
      debug.tree.alias = { found: false };

      // ┌─ STEG 2: Ordet-baserad matchning (förslag) ────────────────────────┐
      debug.steps.push('Step 2: Word match');
      const wordMatchResult = this.findWordMatchWithScore(raw, context.products);
      if (wordMatchResult) {
        debug.tree.wordMatch = { found: true, productId: wordMatchResult.id, score: wordMatchResult.score };
        debug.steps.push(`  ✓ Word match found → productId ${wordMatchResult.id} (score ${wordMatchResult.score})`);

        const preferredUnit =
          this.resolvePreferredUnit(wordMatchResult.id, item.unit, context) ??
          item.unit;

        const result: ParsedReceiptItem = {
          ...item,
          suggestedProductId: wordMatchResult.id,
          suggestedProductName: wordMatchResult.canonicalName ?? wordMatchResult.name,
          unit: preferredUnit,
          matchedVia: 'wordmatch' as const,
        };

        // Lägg på kategori från produkt om den finns
        if (wordMatchResult.categoryRef) {
          result.categorySuggestion = {
            categoryId: wordMatchResult.categoryRef.id,
            categoryName: wordMatchResult.categoryRef.name,
            path: wordMatchResult.categoryRef.name,
            confidence: 'medium' as const,
            usedFallback: false,
          };
        }

        // Gå vidare till kategorisering för wordmatch
        return await this.enrichCategoryForItem(result, context, debug);
      }
      debug.steps.push(`  ✗ No word match`);
      debug.tree.wordMatch = { found: false };

      // ┌─ STEG 3: Regel-baserad kategorisering (no product match) ──────────┐
      return await this.enrichCategoryForItem(
        { ...item, matchedVia: 'none' as const },
        context,
        debug,
      );
    } catch (err) {
      this.logger.warn(`matchAndEnrichReceiptItem error for "${item.rawName}": ${err}`);
      return item;
    }
  }

  private async enrichCategoryForItem(
    item: ParsedReceiptItem,
    context: Pick<MatchingContext, 'categories' | 'aiEnabled'>,
    debug: MatchDebug,
  ): Promise<ParsedReceiptItem> {
    // Kategori-hierarki:
    // 1. Om produktmatchning redan satte kategori, börjar vi med den
    // 2. Försöker regel-baserad kategorisering (HIGH confidence: ersätt, fallback: använd om tom)
    // 3. AI kallas ENDAST om ingen kategori satts än (nextCategory === null)
    // 4. Guards och hard overrides tillämpas på slutresultatet

    debug.steps.push('Step 3: Categorization');

    const signalText = this.buildSignalText(item);
    const signalOrRaw = signalText || item.rawName;

    let nextCategory = item.categorySuggestion ?? null;

    // ┌─ STEG 3A: Försök regel-baserad kategorisering ─────────────────────┐
    debug.steps.push('  Trying rule-based categorization');
    const ruleResult = this.ruleBasedCategorySuggestion(signalOrRaw, context.categories);
    debug.tree.rule = { found: !!ruleResult, path: ruleResult?.path };

    if (ruleResult?.confidence === 'high') {
      const sameAsExisting = nextCategory && nextCategory.categoryId === ruleResult.categoryId;
      if (!sameAsExisting) {
        debug.steps.push(`  ✓ Rule-based HIGH: ${ruleResult.path}`);
        nextCategory = ruleResult;
      } else {
        debug.steps.push(`  ✓ Rule-based HIGH (same as existing): ${ruleResult.path}`);
      }
    } else if (!nextCategory && ruleResult) {
      debug.steps.push(`  ✓ Rule-based fallback: ${ruleResult.path}`);
      nextCategory = ruleResult;
    } else {
      debug.steps.push(`  ✗ Rule-based miss or lower priority`);
    }

    // ┌─ STEG 3B: AI-kategorisering ENDAST som fallback ────────────────────┐
    // AI kallas bara om:
    //   1) nextCategory är fortfarande NULL (regel-baserad misslyckades/fanns inte)
    //   2) User har AI aktiverat (context.aiEnabled === true)
    // AI ersätter ALDRIG redan satta kategorier.
    if (!nextCategory) {
      debug.steps.push('  Trying AI categorization (fallback: no category set yet)');
      if (context.aiEnabled) {
        debug.tree.ai = { called: true };
        try {
          nextCategory = await this.aiService.suggestCategory(item.rawName, context.categories);
          debug.steps.push(`  ✓ AI suggestion: ${nextCategory.path}`);
        } catch (err) {
          debug.steps.push(`  ✗ AI failed: ${err}`);
          debug.tree.ai = { called: true, error: String(err) };
        }
      } else {
        debug.steps.push(`  ✗ AI disabled for user`);
        debug.tree.ai = { called: false };
      }
    } else {
      debug.steps.push(`  ⊘ AI skipped (category already set: ${nextCategory.path})`);
      debug.tree.ai = { called: false, reason: 'category_already_set' };
    }

    // ┌─ Contradiction guard (final sanity check) ────────────────────────┐
    if (nextCategory) {
      debug.steps.push('  Applying contradiction guard');
      const beforePath = nextCategory.path;
      const guardedCategory = this.applyContradictionGuard(signalOrRaw, nextCategory, context.categories);
      if (guardedCategory && guardedCategory.path !== beforePath) {
        debug.steps.push(`  ⚠️ Guard remapped: ${beforePath} → ${guardedCategory.path}`);
        nextCategory = guardedCategory;
        debug.tree.guard = { applied: true, oldPath: beforePath, newPath: guardedCategory.path };
      } else {
        debug.steps.push(`  ✓ Guard OK`);
      }
    }

    // ┌─ Hard overrides (special rules for problematic cases) ─────────────┐
    if (nextCategory) {
      debug.steps.push('  Applying hard overrides');
      const beforePath = nextCategory.path;
      const finalCategory = this.applyHardCategoryOverrides(signalOrRaw, nextCategory, context.categories);
      if (finalCategory && finalCategory.path !== beforePath) {
        debug.steps.push(`  ⚠️ Override applied: ${beforePath} → ${finalCategory.path}`);
        nextCategory = finalCategory;
        debug.tree.hardOverride = { applied: true, oldPath: beforePath, newPath: finalCategory.path };
      } else {
        debug.steps.push(`  ✓ No hard override needed`);
      }
    }

    if (nextCategory) {
      debug.steps.push(`✅ FINAL: ${nextCategory.path} (${nextCategory.confidence})`);
    } else {
      debug.steps.push(`❌ FINAL: No category assigned`);
    }

    if (this.shouldTraceDecision(signalOrRaw)) {
      this.logger.log(`[ReceiptDecision] ${item.rawName}\n${debug.steps.join('\n')}`);
    }

    return nextCategory ? { ...item, categorySuggestion: nextCategory } : item;
  }

  // Helper: findWordMatch som returnerar både product OCH score
  private findWordMatchWithScore(
    raw: string,
    products: Array<{
      id: number;
      name: string;
      canonicalName: string | null;
      categoryRef: { id: number; name: string } | null;
    }>,
  ): (typeof products[0] & { score: number }) | undefined {
    const rawWords = tokenize(raw);
    if (rawWords.length === 0) return undefined;

    const rawWordSet = new Set(rawWords);
    const rawWordsNorm = rawWords.map(normalizeToken);
    const rawWordSetNorm = new Set(rawWordsNorm);

    let best: (typeof products[0] & { score: number }) | undefined;

    for (const product of products) {
      const productWords = tokenize(product.canonicalName ?? product.name);
      if (productWords.length === 0) continue;

      let score = 0;
      let exactStrong = 0;
      let exactAny = 0;
      let partialStrong = 0;

      const phrase = (product.canonicalName ?? product.name).toLowerCase();
      if (raw.includes(phrase)) {
        score += 5;
      }

      for (const pw of productWords) {
        const isWeak = WEAK_DESCRIPTORS.has(pw);
        const pwNorm = normalizeToken(pw);

        if (rawWordSet.has(pw) || rawWordSetNorm.has(pwNorm)) {
          exactAny += 1;
          if (isWeak) {
            score += 1;
          } else {
            exactStrong += 1;
            score += 8;
          }
          continue;
        }

        if (pw.length < 4) continue;

        const hasPartial =
          rawWords.some((rw) => rw.includes(pw) || pw.includes(rw)) ||
          rawWordsNorm.some((rw) => rw.includes(pwNorm) || pwNorm.includes(rw));
        if (!hasPartial) continue;

        if (isWeak) continue;

        partialStrong += 1;
        score += 3;
      }

      const hasLongPartial = partialStrong >= 1 && productWords.some((pw) => pw.length >= 5);
      const hasStrongSignal = exactStrong >= 1 || exactAny + partialStrong >= 2 || hasLongPartial;
      if (!hasStrongSignal) continue;

      if (score < 8) continue;

      if (!best || score > best.score) {
        best = { ...product, score };
      }
    }

    return best;
  }

  private shouldTraceDecision(signalText: string): boolean {
    const envFlag = (process.env.RECEIPT_TRACE_DECISIONS ?? '').toLowerCase();
    if (envFlag === '1' || envFlag === 'true' || envFlag === 'yes') {
      return true;
    }

    const normalized = normalizeForRules(signalText);
    return hasPorkLikeSignal(normalized);
  }

  private resolvePorkCategory(
    categories: Awaited<ReturnType<CategoriesService['findFlattened']>>,
  ) {
    return (
      categories.find(
        (c) =>
          c.name.toLowerCase() === 'fläsk' &&
          c.path.toLowerCase().startsWith('kött, chark & fågel > kött > '),
      ) ||
      categories.find(
        (c) =>
          c.name.toLowerCase() === 'kött' &&
          c.path.toLowerCase() === 'kött, chark & fågel > kött',
      ) ||
      categories.find((c) => c.path.toLowerCase() === 'kött, chark & fågel')
    );
  }

  private resolveBreadCategory(
    categories: Awaited<ReturnType<CategoriesService['findFlattened']>>,
  ) {
    return (
      categories.find(
        (c) =>
          c.name.toLowerCase() === 'rostbröd' &&
          c.path.toLowerCase().startsWith('bröd & kakor > bröd > '),
      ) ||
      categories.find(
        (c) =>
          c.name.toLowerCase() === 'bröd' &&
          c.path.toLowerCase() === 'bröd & kakor > bröd',
      ) ||
      categories.find((c) => c.path.toLowerCase() === 'bröd & kakor')
    );
  }

  private applyHardCategoryOverrides(
    signalText: string,
    suggestion: CategorySuggestion,
    categories: Awaited<ReturnType<CategoriesService['findFlattened']>>,
  ): CategorySuggestion {
    const normalized = normalizeForRules(signalText);
    const hasBaconLikeSignal = hasPorkLikeSignal(normalized);

    if (!hasBaconLikeSignal) return suggestion;

    const l3Pork = this.resolvePorkCategory(categories);
    if (!l3Pork) {
      this.logger.warn(`Hard-override: pork signal hittad men ingen köttkategori kunde hittas för "${signalText}"`);
      return suggestion;
    }

    if (suggestion.categoryId === l3Pork.id) return suggestion;

    this.logger.log(
      `Hard-override: "${signalText}" remappas från "${suggestion.path}" till "${l3Pork.path}"`,
    );
    return {
      categoryId: l3Pork.id,
      categoryName: l3Pork.name,
      path: l3Pork.path,
      confidence: 'high',
      usedFallback: true,
    };
  }

  private ruleBasedCategorySuggestion(
    rawName: string,
    categories: Awaited<ReturnType<CategoriesService['findFlattened']>>,
  ): CategorySuggestion | null {
    const normalized = normalizeForRules(rawName);
    const findCategory = (opts: {
      name: string;
      startsWith?: string;
      includes?: string;
    }) =>
      categories.find((c) => {
        const cName = c.name.toLowerCase();
        const cPath = c.path.toLowerCase();
        if (cName !== opts.name.toLowerCase()) return false;
        if (opts.startsWith && !cPath.startsWith(opts.startsWith.toLowerCase())) return false;
        if (opts.includes && !cPath.includes(opts.includes.toLowerCase())) return false;
        return true;
      });

    const toSuggestion = (
      cat: { id: number; name: string; path: string } | undefined,
      confidence: 'high' | 'medium' = 'high',
    ): CategorySuggestion | null => {
      if (!cat) return null;
      return {
        categoryId: cat.id,
        categoryName: cat.name,
        path: cat.path,
        confidence,
        usedFallback: false,
      };
    };

    // ── Regel: Kött/chark (bacon/fläsk m.m.) ────────────────────────────
    const hasPorkSignal = hasPorkLikeSignal(normalized);

    const hasToastBreadSignal = hasBreadLikeSignal(normalized);

    if (hasToastBreadSignal) {
      const bread = this.resolveBreadCategory(categories);
      const hit = toSuggestion(bread, 'high');
      if (hit) return hit;
    }

    if (hasPorkSignal) {
      const l3Pork = this.resolvePorkCategory(categories);
      const hit = toSuggestion(l3Pork, 'high');
      if (hit) return hit;
    }

    // ── Regel: Choklad & spreads (Nutella, sjokolade, m.m.) ─────────────
    const hasChocolateSignal =
      normalized.includes('nutella') ||
      normalized.includes('chocolate') ||
      normalized.includes('choklad') ||
      normalized.includes('sjokolade') ||
      normalized.includes('kakao') ||
      normalized.includes('spreads');

    if (hasChocolateSignal) {
      const chocolate = findCategory({
        name: 'choklad & spreads',
        includes: 'sötsaker',
      });
      const hit = toSuggestion(chocolate, 'high');
      if (hit) return hit;
    }

    // ── Regel: Korvfamiljen ─────────────────────────────────────────────
    const hasSausageSignal =
      /\bkorv\b/.test(normalized) ||
      /\bfalukorv\b/.test(normalized) ||
      /\bchorizo\b/.test(normalized) ||
      /\bbratwurst\b/.test(normalized) ||
      /\bwienerkorv\b/.test(normalized) ||
      /\bgrillkorv\b/.test(normalized) ||
      /\bprinskorv\b/.test(normalized) ||
      /\bolkorv\b/.test(normalized);

    if (hasSausageSignal) {
      const isVegetarian =
        /\bvegetarisk\b/.test(normalized) ||
        /\bvegansk\b/.test(normalized) ||
        /\bvego\b/.test(normalized);
      if (isVegetarian) {
        const vegSausage = findCategory({
          name: 'vegetarisk korv',
          startsWith: 'kött, chark & fågel > korv > ',
        });
        const hit = toSuggestion(vegSausage, 'high');
        if (hit) return hit;
      }

      if (/\bolkorv\b/.test(normalized)) {
        const beerSausage = findCategory({
          name: 'ölkorv',
          startsWith: 'kött, chark & fågel > korv > ',
        });
        const hit = toSuggestion(beerSausage, 'high');
        if (hit) return hit;
      }

      const sausageGeneral = findCategory({
        name: 'grill, kok- & kryddkorv',
        startsWith: 'kött, chark & fågel > korv > ',
      });
      const hit = toSuggestion(sausageGeneral, 'high');
      if (hit) return hit;
    }

    // ── Regel: Fågel (färsk/fryst) ──────────────────────────────────────
    const hasPoultrySignal =
      /\bkyckling\b/.test(normalized) ||
      /\bkalkon\b/.test(normalized) ||
      /\bdrumsticks?\b/.test(normalized) ||
      /\bling?file\b/.test(normalized) ||
      /\blarfile\b/.test(normalized) ||
      /\bkycklinglar\b/.test(normalized);

    if (hasPoultrySignal) {
      const isFrozen = /\bfryst\b/.test(normalized) || /\bdjupfryst\b/.test(normalized);
      if (isFrozen) {
        const frozenPoultry = findCategory({
          name: 'fryst fågel',
          startsWith: 'kött, chark & fågel > fågel > ',
        });
        const hit = toSuggestion(frozenPoultry, 'high');
        if (hit) return hit;
      }

      const freshPoultry = findCategory({
        name: 'färsk fågel',
        startsWith: 'kött, chark & fågel > fågel > ',
      });
      const hit = toSuggestion(freshPoultry, 'high');
      if (hit) return hit;
    }

    // ── Regel: Pålägg (korv/salami vs skivat) ───────────────────────────
    const hasColdCutSignal =
      /\bpalagg\b/.test(normalized) ||
      /\bpalegg\b/.test(normalized) ||
      /\bskivad\b/.test(normalized) ||
      /\bsalami\b/.test(normalized) ||
      /\bmedvurst\b/.test(normalized) ||
      /\bpastrami\b/.test(normalized) ||
      /\bskinka\b/.test(normalized);

    if (hasColdCutSignal) {
      const hasSlicedSausageSignal =
        /\bsalami\b/.test(normalized) ||
        /\bmedvurst\b/.test(normalized) ||
        /\bpastrami\b/.test(normalized);
      if (hasSlicedSausageSignal) {
        const salamiColdCut = findCategory({
          name: 'korv & salami',
          startsWith: 'kött, chark & fågel > pålägg > ',
        });
        const hit = toSuggestion(salamiColdCut, 'high');
        if (hit) return hit;
      }

      const slicedColdCut = findCategory({
        name: 'skivat pålägg',
        startsWith: 'kött, chark & fågel > pålägg > ',
      });
      const hit = toSuggestion(slicedColdCut, 'high');
      if (hit) return hit;
    }

    // ── Regel: Färs ──────────────────────────────────────────────────────
    const hasMinceSignal =
      /\bfars\b/.test(normalized) ||
      /\bfarse\b/.test(normalized) ||
      /\bmince\b/.test(normalized) ||
      /\bköttfärs\b/.test(rawName.toLowerCase());

    if (hasMinceSignal && !hasPoultrySignal) {
      const mince = findCategory({
        name: 'köttfärs',
        startsWith: 'kött, chark & fågel > kött > ',
      });
      const hit = toSuggestion(mince, 'high');
      if (hit) return hit;
    }

    // ── Regel: Nöt/kalv (hela detaljer) ─────────────────────────────────
    const hasBeefVealSignal =
      /\bnot\b/.test(normalized) ||
      /\bkalv\b/.test(normalized) ||
      /\bbiff\b/.test(normalized) ||
      /\bentrecote\b/.test(normalized) ||
      /\brostbiff\b/.test(normalized) ||
      /\bryggbiff\b/.test(normalized);

    if (hasBeefVealSignal && !hasMinceSignal) {
      const beefVeal = findCategory({
        name: 'nöt & kalv',
        startsWith: 'kött, chark & fågel > kött > ',
      });
      const hit = toSuggestion(beefVeal, 'high');
      if (hit) return hit;
    }

    // ── Regel: Pasta (inkl. italienska formatnamn) ─────────────────────
    const hasPastaSignal =
      /\bmezze\b/.test(normalized) ||
      /\bmaniche\b/.test(normalized) ||
      /\bpenne\b/.test(normalized) ||
      /\brigatoni\b/.test(normalized) ||
      /\bfusilli\b/.test(normalized) ||
      /\bspaghetti\b/.test(normalized) ||
      /\btagliatelle\b/.test(normalized) ||
      /\bmakaron\w*\b/.test(normalized) ||
      /\bgnocchi\b/.test(normalized) ||
      /\blasagne\b/.test(normalized) ||
      /\bpasta\b/.test(normalized);

    if (hasPastaSignal) {
      const freshPasta = findCategory({
        name: 'färsk pasta',
        startsWith: 'skafferi > pasta, ris & matgryn > ',
      });
      if (/\bfarsk\b/.test(normalized) || /\bfresh\b/.test(normalized)) {
        const freshHit = toSuggestion(freshPasta, 'high');
        if (freshHit) return freshHit;
      }

      const pasta = findCategory({
        name: 'pasta',
        startsWith: 'skafferi > pasta, ris & matgryn > ',
      });
      const pastaHit = toSuggestion(pasta, 'high');
      if (pastaHit) return pastaHit;
    }

    // ── Regel: Grädde/matlagningsgrädde (icke-allergi) ─────────────────
    const hasCreamSignal =
      /\bvispgradde\b/.test(normalized) ||
      /\bmatlagningsgradde\b/.test(normalized) ||
      /\bgradde\b/.test(normalized) ||
      /\bcreme\s+fraiche\b/.test(normalized) ||
      /\bgraddfil\b/.test(normalized);

    const hasPlantOrAllergySignal =
      /\blaktosfri\b/.test(normalized) ||
      /\bvegetabilisk\b/.test(normalized) ||
      /\bhavre\b/.test(normalized) ||
      /\bsoja\b/.test(normalized) ||
      /\brisdryck\b/.test(normalized) ||
      /\bplant\b/.test(normalized);

    if (hasCreamSignal && !hasPlantOrAllergySignal) {
      const l3Cream = findCategory({
        name: 'grädde',
        startsWith: 'mejeri, ost & ägg > matlagning > ',
      });
      const l3Hit = toSuggestion(l3Cream, 'high');
      if (l3Hit) return l3Hit;

      const l2CookingDairy = findCategory({
        name: 'matlagning',
        startsWith: 'mejeri, ost & ägg > ',
      });
      const hit = toSuggestion(l2CookingDairy, 'high');
      if (hit) return hit;
    }

    // ── Regel: Vanlig mjölk (inte laktosfri/allergi) ───────────────────
    const hasMilkSignal =
      /\bmjolk\b/.test(normalized) ||
      /\bstandardmjolk\b/.test(normalized) ||
      /\bstandmjolk\b/.test(normalized) ||
      /\besl\b/.test(normalized);
    const hasLactoseFreeSignal =
      /\blaktosfri\b/.test(normalized) ||
      /\blactose\s*free\b/.test(normalized);

    if (hasMilkSignal && !hasPlantOrAllergySignal && !hasLactoseFreeSignal) {
      const l3StandardMilk = findCategory({
        name: 'standardmjölk',
        startsWith: 'mejeri, ost & ägg > mjölk > ',
      });
      const hit = toSuggestion(l3StandardMilk, 'high');
      if (hit) return hit;

      const l2Milk = findCategory({
        name: 'mjölk',
        startsWith: 'mejeri, ost & ägg > ',
      });
      const fallbackHit = toSuggestion(l2Milk, 'high');
      if (fallbackHit) return fallbackHit;
    }

    // ── Regel: Ägg ──────────────────────────────────────────────────────
    const hasEggSignal =
      /\bagg\b/.test(normalized) ||
      /\begg\b/.test(normalized) ||
      /\binne\b/.test(normalized) ||
      /\b24p\b/.test(normalized);

    if (hasEggSignal) {
      const l2Egg = categories.find(
        (c) =>
          c.name.toLowerCase() === 'ägg' &&
          c.path.toLowerCase() === 'mejeri, ost & ägg > ägg',
      );
      const l2EggHit = toSuggestion(l2Egg, 'high');
      if (l2EggHit) return l2EggHit;

      const l1DairyEgg = categories.find(
        (c) => c.path.toLowerCase() === 'mejeri, ost & ägg',
      );
      const l1DairyEggHit = toSuggestion(l1DairyEgg, 'high');
      if (l1DairyEggHit) return l1DairyEggHit;
    }

    // ── Regel: Juice/fruktdryck/smoothie ───────────────────────────────
    const hasJuiceSignal =
      /\bjuice\b/.test(normalized) ||
      /\bnektar\b/.test(normalized) ||
      /\bfruktdryck\b/.test(normalized) ||
      /\bsmoothie\b/.test(normalized) ||
      /\bmultivitamin\b/.test(normalized);

    if (hasJuiceSignal) {
      const l3ColdJuice = findCategory({
        name: 'kyld juice & nektar',
        startsWith: 'dryck > juice, fruktdryck & smoothie > ',
      });
      const l3Hit = toSuggestion(l3ColdJuice, 'high');
      if (l3Hit) return l3Hit;

      const l2Juice = findCategory({
        name: 'juice, fruktdryck & smoothie',
        startsWith: 'dryck > ',
      });
      const l2Hit = toSuggestion(l2Juice, 'high');
      if (l2Hit) return l2Hit;
    }

    // ── Regel: Te ────────────────────────────────────────────────────────
    const isTea =
      /\bte\b/.test(normalized) ||
      /\btea\b/.test(normalized) ||
      /\bchai\b/.test(normalized) ||
      /\btepa(se|k|r)?\b/.test(normalized);

    if (isTea) {
      const l3Te = categories.find(
        (c) => c.name.toLowerCase() === 'te' && c.path.toLowerCase().includes('te & choklad'),
      );
      const l3TeHit = toSuggestion(l3Te, 'high');
      if (l3TeHit) return l3TeHit;
      const l2TeChoklad = categories.find(
        (c) => c.name.toLowerCase() === 'te & choklad' && c.path.toLowerCase().startsWith('dryck'),
      );
      const l2TeChokladHit = toSuggestion(l2TeChoklad, 'medium');
      if (l2TeChokladHit) return l2TeChokladHit;
    }

    // ── Regel: Kaffebröd ─────────────────────────────────────────────────
    const isKaffebrod =
      /\bkaffebrod\b/.test(normalized) ||
      /\bwienerbrod\b/.test(normalized) ||
      /\bdonut\b/.test(normalized) ||
      /\bmunk\b/.test(normalized) ||
      /\bcroissant\b/.test(normalized) ||
      /\bkanelbulle\b/.test(normalized) ||
      /\bbakelse\b/.test(normalized) ||
      /\bsemla\b/.test(normalized) ||
      /\bdammsugare\b/.test(normalized) ||
      /\bkladdkaka\b/.test(normalized) ||
      /\bmuffin\b/.test(normalized) ||
      /\bcupcake\b/.test(normalized) ||
      /\bchokladboll\b/.test(normalized);

    if (isKaffebrod) {
      const l3Kaffebrod = categories.find(
        (c) => c.name.toLowerCase() === 'kaffebröd' && c.path.toLowerCase().includes('kondis & fika'),
      );
      const l3KaffebrodHit = toSuggestion(l3Kaffebrod, 'high');
      if (l3KaffebrodHit) return l3KaffebrodHit;
      const l2Kondis = categories.find(
        (c) => c.name.toLowerCase() === 'kondis & fika' && c.path.toLowerCase().startsWith('bröd & kakor'),
      );
      const l2KondisHit = toSuggestion(l2Kondis, 'medium');
      if (l2KondisHit) return l2KondisHit;
    }

    // ── Regel: Godis/chokladkakor ──────────────────────────────────────
    const isChocolateBar =
      /\bsnickers\b/.test(normalized) ||
      /\bmars\b/.test(normalized) ||
      /\btwix\b/.test(normalized) ||
      /\bbounty\b/.test(normalized) ||
      /\bkitkat\b/.test(normalized) ||
      /\bdajm\b/.test(normalized) ||
      /\bjapp\b/.test(normalized);

    if (isChocolateBar) {
      const l3ChocolateBars = findCategory({
        name: 'chokladkakor & rullar',
        startsWith: 'glass, godis & snacks > choklad > ',
      });
      const hit = toSuggestion(l3ChocolateBars, 'high');
      if (hit) return hit;
    }

    const isCandyBagLike =
      /\bnappar\b/.test(normalized) ||
      /\bgodispas\w*\b/.test(normalized);

    if (isCandyBagLike) {
      const l3CandyBag = findCategory({
        name: 'godispåsar',
        startsWith: 'glass, godis & snacks > godis > ',
      });
      const hit = toSuggestion(l3CandyBag, 'high');
      if (hit) return hit;
    }

    // ── Regel: Potatis (färsk) ─────────────────────────────────────────
    const hasPotatoSignal = /\bpotatis\b/.test(normalized);
    const hasFrozenPotatoSignal =
      /\bfryst\b/.test(normalized) ||
      /\bdjupfryst\b/.test(normalized) ||
      /\bpommes\b/.test(normalized) ||
      /\bstrips?\b/.test(normalized);

    if (hasPotatoSignal && !hasFrozenPotatoSignal) {
      const l3Potato = findCategory({
        name: 'potatis',
        startsWith: 'frukt & grönt > potatis & rotsaker > ',
      });
      const l3Hit = toSuggestion(l3Potato, 'high');
      if (l3Hit) return l3Hit;
    }

    // ── Regel: Laktosfri/växtbaserad mejeri ──────────────────────────────
    const isCookingBase =
      /\bmatlagningsbas\b/.test(normalized) ||
      /\bmatlagnings\b/.test(normalized) ||
      /\bplant\s+cream\b/.test(normalized) ||
      /\bcreme\s+fraiche\b/.test(normalized) ||
      /\bgradde\b/.test(normalized) ||
      /\bvispgradde\b/.test(normalized);

    const isPlantOrAllergy =
      /\blaktosfri\b/.test(normalized) ||
      /\bvegetabilisk\b/.test(normalized) ||
      /\bhavre\b/.test(normalized) ||
      /\bsoja\b/.test(normalized) ||
      /\brisdryck\b/.test(normalized) ||
      /\bplant\b/.test(normalized);

    if (!isCookingBase || !isPlantOrAllergy) return null;

    const l3AllergyCooking = categories.find(
      (c) =>
        c.name.toLowerCase() === 'allergi matlagning' &&
        c.path.toLowerCase().startsWith('matlagning > '),
    );
    const l3AllergyCookingHit = toSuggestion(l3AllergyCooking, 'high');
    if (l3AllergyCookingHit) return l3AllergyCookingHit;

    const l2Cooking = categories.find(
      (c) =>
        c.name.toLowerCase() === 'matlagning' &&
        c.path.toLowerCase() === 'mejeri, ost & ägg > matlagning',
    );
    const l2CookingHit = toSuggestion(l2Cooking, 'medium');
    if (l2CookingHit) return l2CookingHit;

    return null;
  }

  private applyContradictionGuard(
    rawName: string,
    suggestion: CategorySuggestion,
    categories: Awaited<ReturnType<CategoriesService['findFlattened']>>,
  ): CategorySuggestion {
    const normalized = normalizeForRules(rawName);
    const hasPorkSignal = hasPorkLikeSignal(normalized);

    if (hasPorkSignal) {
      const aiPath = suggestion.path.toLowerCase();
      const isClearlyWrongBranch =
        aiPath.includes('köttbullar & färsprodukter') || aiPath.includes('köttfärs');

      if (!isClearlyWrongBranch) return suggestion;

      const l3Pork = this.resolvePorkCategory(categories);
      if (!l3Pork) return suggestion;

      this.logger.log(
        `AI contradiction-guard: "${rawName}" remappas från "${suggestion.path}" till "${l3Pork.path}"`,
      );
      return {
        categoryId: l3Pork.id,
        categoryName: l3Pork.name,
        path: l3Pork.path,
        confidence: 'high',
        usedFallback: true,
      };
    }

    const hasMilkSignal =
      /\bmjolk\b/.test(normalized) ||
      /\bstandardmjolk\b/.test(normalized) ||
      /\bstandmjolk\b/.test(normalized) ||
      /\besl\b/.test(normalized);
    const hasLactoseFreeSignal =
      /\blaktosfri\b/.test(normalized) ||
      /\blactose\s*free\b/.test(normalized);

    if (hasMilkSignal && !hasLactoseFreeSignal) {
      const isWrongLactoseFreeBranch =
        suggestion.path.toLowerCase().includes('allergi mejeri > laktosfri mjölk');
      if (isWrongLactoseFreeBranch) {
        const l3StandardMilk = categories.find(
          (c) =>
            c.name.toLowerCase() === 'standardmjölk' &&
            c.path.toLowerCase().startsWith('mejeri, ost & ägg > mjölk > '),
        );
        if (l3StandardMilk) {
          this.logger.log(
            `AI contradiction-guard: "${rawName}" remappas från "${suggestion.path}" till "${l3StandardMilk.path}"`,
          );
          return {
            categoryId: l3StandardMilk.id,
            categoryName: l3StandardMilk.name,
            path: l3StandardMilk.path,
            confidence: 'high',
            usedFallback: true,
          };
        }
      }
    }

    const hasEggSignal =
      /\bagg\b/.test(normalized) ||
      /\begg\b/.test(normalized) ||
      /\binne\b/.test(normalized) ||
      /\b24p\b/.test(normalized);

    if (hasEggSignal && suggestion.path.toLowerCase().includes('allergi mejeri')) {
      const l2Egg = categories.find(
        (c) =>
          c.name.toLowerCase() === 'ägg' &&
          c.path.toLowerCase() === 'mejeri, ost & ägg > ägg',
      );
      if (l2Egg) {
        this.logger.log(
          `AI contradiction-guard: "${rawName}" remappas från "${suggestion.path}" till "${l2Egg.path}"`,
        );
        return {
          categoryId: l2Egg.id,
          categoryName: l2Egg.name,
          path: l2Egg.path,
          confidence: 'high',
          usedFallback: true,
        };
      }

      const l1DairyEgg = categories.find(
        (c) => c.path.toLowerCase() === 'mejeri, ost & ägg',
      );
      if (l1DairyEgg) {
        this.logger.log(
          `AI contradiction-guard: "${rawName}" remappas från "${suggestion.path}" till "${l1DairyEgg.path}"`,
        );
        return {
          categoryId: l1DairyEgg.id,
          categoryName: l1DairyEgg.name,
          path: l1DairyEgg.path,
          confidence: 'high',
          usedFallback: true,
        };
      }
    }

    const hasCreamSignal =
      /\bvispgradde\b/.test(normalized) ||
      /\bmatlagningsgradde\b/.test(normalized) ||
      /\bgradde\b/.test(normalized) ||
      /\bcreme\s+fraiche\b/.test(normalized) ||
      /\bgraddfil\b/.test(normalized);
    const hasPlantOrAllergySignal =
      /\blaktosfri\b/.test(normalized) ||
      /\bvegetabilisk\b/.test(normalized) ||
      /\bhavre\b/.test(normalized) ||
      /\bsoja\b/.test(normalized) ||
      /\brisdryck\b/.test(normalized) ||
      /\bplant\b/.test(normalized);

    if (hasCreamSignal && !hasPlantOrAllergySignal) {
      const aiPath = suggestion.path.toLowerCase();
      const isOutsideDairy = !aiPath.startsWith('mejeri, ost & ägg > matlagning');
      if (!isOutsideDairy) return suggestion;

      const l2CookingDairy = categories.find(
        (c) =>
          c.name.toLowerCase() === 'matlagning' &&
          c.path.toLowerCase() === 'mejeri, ost & ägg > matlagning',
      );
      if (!l2CookingDairy) return suggestion;

      const l3Cream = categories.find(
        (c) =>
          c.name.toLowerCase() === 'grädde' &&
          c.path.toLowerCase().startsWith('mejeri, ost & ägg > matlagning > '),
      );

      if (l3Cream) {
        this.logger.log(
          `AI contradiction-guard: "${rawName}" remappas från "${suggestion.path}" till "${l3Cream.path}"`,
        );
        return {
          categoryId: l3Cream.id,
          categoryName: l3Cream.name,
          path: l3Cream.path,
          confidence: 'high',
          usedFallback: true,
        };
      }

      this.logger.log(
        `AI contradiction-guard: "${rawName}" remappas från "${suggestion.path}" till "${l2CookingDairy.path}"`,
      );
      return {
        categoryId: l2CookingDairy.id,
        categoryName: l2CookingDairy.name,
        path: l2CookingDairy.path,
        confidence: 'high',
        usedFallback: true,
      };
    }

    const hasToastBreadSignal = hasBreadLikeSignal(normalized);

    if (hasToastBreadSignal) {
      const aiPath = suggestion.path.toLowerCase();
      const isOutsideBread = !aiPath.startsWith('bröd & kakor > bröd');
      if (!isOutsideBread) return suggestion;

      const bread = this.resolveBreadCategory(categories);
      if (!bread) return suggestion;

      this.logger.log(
        `AI contradiction-guard: "${rawName}" remappas från "${suggestion.path}" till "${bread.path}"`,
      );
      return {
        categoryId: bread.id,
        categoryName: bread.name,
        path: bread.path,
        confidence: 'high',
        usedFallback: true,
      };
    }

    return suggestion;
  }
}
