import {
  BadRequestException,
  Injectable,
  Logger,
  ServiceUnavailableException,
} from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { ParsedReceiptItem } from './dto/parsed-receipt-item.dto';
import { AiService, CategorySuggestion } from '../ai/ai.service';
import { CategoriesService } from '../categories/categories.service';

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

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);
}

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();
}

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

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

  async parseReceipt(file: Express.Multer.File, _isPremium = false, userId?: number): Promise<ParsedReceiptItem[]> {
    // Steg 1: Delegera AI-parsning till microservice-importer
    const rawItems = await this.parseReceiptViaImporter(file);

    // Steg 2: Matchning mot produktdatabas (kräver DB — stannar i recipe-app)
    const matched = await this.matchProducts(rawItems, userId);

    // Steg 3: Regel + AI-kategorisering för alla användare
    return this.enrichWithAiCategories(matched);
  }

  private async parseReceiptViaImporter(file: Express.Multer.File): Promise<ParsedReceiptItem[]> {
    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
      }
      this.logger.error(`Importer-api kvittoparsfel: ${message}`);
      if (response.status >= 400 && response.status < 500) {
        throw new BadRequestException(message);
      }
      throw new ServiceUnavailableException(message);
    }

    return response.json() as Promise<ParsedReceiptItem[]>;
  }

  private async matchProducts(
    items: ParsedReceiptItem[],
    userId?: number,
  ): Promise<ParsedReceiptItem[]> {
    // Hämta alias och produkter parallellt — filtrera på userId om angivet
    const productFilter = userId ? { isActive: true, ownerId: userId } : { isActive: true };
    const aliasFilter = userId
      ? { product: { ownerId: userId } }
      : {};
    const [aliases, products] = await Promise.all([
      this.prisma.receiptAlias.findMany({
        where: aliasFilter,
        select: { receiptName: true, productId: true, product: { select: { id: true, name: true, canonicalName: true, categoryId: true, categoryRef: { select: { id: true, name: true } } } } },
      }),
      this.prisma.product.findMany({
        where: productFilter,
        select: { id: true, name: true, canonicalName: true, categoryId: true, categoryRef: { select: { id: true, name: true } } },
      }),
    ]);

    return items.map((item) => {
      const raw = (item.rawName ?? '').toLowerCase().trim();
      if (!raw) return item;

      // 1. Alias-match (säker, användaren behöver inte bekräfta)
      const alias = aliases.find((a) => a.receiptName === raw);
      if (alias) {
        const cat = alias.product.categoryRef;
        return {
          ...item,
          matchedProductId: alias.product.id,
          matchedProductName: alias.product.canonicalName ?? alias.product.name,
          ...(cat ? { categorySuggestion: { categoryId: cat.id, categoryName: cat.name, path: cat.name, confidence: 'high' as const, usedFallback: false } } : {}),
        };
      }

      // 2. Ordbaserad matchning (förslag, kräver bekräftelse)
      const suggestion = this.findWordMatch(raw, products);
      if (!suggestion) {
        return { ...item };
      }
      const cat = suggestion.categoryRef;
      return {
        ...item,
        suggestedProductId: suggestion.id,
        suggestedProductName: suggestion.canonicalName ?? suggestion.name,
        ...(cat ? { categorySuggestion: { categoryId: cat.id, categoryName: cat.name, path: cat.name, confidence: 'medium' as const, usedFallback: false } } : {}),
      };
    });
  }

  private findWordMatch(
    raw: string,
    products: { id: number; name: string; canonicalName: string | null; categoryId: number | null; categoryRef: { id: number; name: string } | null }[],
  ): { id: number; name: string; canonicalName: string | null; categoryId: number | null; categoryRef: { id: number; name: string } | null } | undefined {
    // Dela upp kvittonamnet i ord (min 3 tecken)
    const rawWords = tokenize(raw);
    if (rawWords.length === 0) return undefined;

    const rawWordSet = new Set(rawWords);
    // Normaliserade versioner (utan diakritik) för att hantera t.ex. gradde == grädde
    const rawWordsNorm = rawWords.map(normalizeToken);
    const rawWordSetNorm = new Set(rawWordsNorm);

    let best:
      | { product: { id: number; name: string; canonicalName: string | null; categoryId: number | null; categoryRef: { id: number; name: string } | null }; 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;
        }

        // Delmatchning tillåts bara för ord med minst 4 tecken.
        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) {
          // Deskriptiva ord (t.ex. rökt) ska inte driva förslag ensamma.
          continue;
        }

        partialStrong += 1;
        score += 3;
      }

      // Kräv antingen minst ett starkt exakt ord, eller flera samverkande signaler.
      // Undantag: ett enstaka starkt partiellt ord (>=5 tecken) räcker, t.ex. vispgrädde → grädde.
      const hasLongPartial = partialStrong >= 1 && productWords.some((pw) => pw.length >= 5);
      const hasStrongSignal = exactStrong >= 1 || exactAny + partialStrong >= 2 || hasLongPartial;
      if (!hasStrongSignal) continue;

      // Tröskel för att undvika svaga enkelträffar.
      if (score < 8) continue;

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

    return best?.product;
  }

  private async enrichWithAiCategories(items: ParsedReceiptItem[]): Promise<ParsedReceiptItem[]> {
    let categories: Awaited<ReturnType<CategoriesService['findFlattened']>>;
    try {
      categories = await this.categoriesService.findFlattened();
    } catch {
      return items; // Om kategoritjänsten är otillgänglig, returnera utan AI-förslag
    }

    const enriched: ParsedReceiptItem[] = [];
    for (const item of items) {
      if (!item.rawName) {
        enriched.push(item);
        continue;
      }

      try {
        const signalText = [
          item.rawName,
          item.matchedProductName,
          item.suggestedProductName,
        ]
          .filter((v): v is string => typeof v === 'string' && v.trim().length > 0)
          .join(' ');

        const trace: string[] = [];
        const traceEnabled = this.shouldTraceDecision(signalText || item.rawName);
        const pushTrace = (msg: string) => {
          if (traceEnabled) trace.push(msg);
        };

        pushTrace(`start raw="${item.rawName}" signal="${signalText || item.rawName}"`);
        pushTrace(
          `match matchedProductId=${item.matchedProductId ?? 'null'} suggestedProductId=${item.suggestedProductId ?? 'null'}`,
        );
        if (item.categorySuggestion) {
          pushTrace(
            `incoming category="${item.categorySuggestion.path}" confidence=${item.categorySuggestion.confidence} fallback=${item.categorySuggestion.usedFallback}`,
          );
        }

        const byRule = this.ruleBasedCategorySuggestion(signalText || item.rawName, categories);
        if (byRule) {
          pushTrace(`rule hit -> "${byRule.path}" (${byRule.confidence})`);
        } else {
          pushTrace('rule miss');
        }
        let nextSuggestion = item.categorySuggestion ?? null;

        const isTrustedSuggestion =
          nextSuggestion?.confidence === 'high' && !nextSuggestion.usedFallback;

        // Regel med stark signal får överstyra svaga förslag (och även felaktiga matchningsförslag)
        if (byRule?.confidence === 'high') {
          const sameAsCurrent =
            nextSuggestion != null && nextSuggestion.categoryId === byRule.categoryId;
          if (!sameAsCurrent && (!isTrustedSuggestion || nextSuggestion == null)) {
            nextSuggestion = byRule;
            pushTrace(`rule applied -> "${byRule.path}"`);
          }

          // Om regler säger en annan kategori än ett redan "trusted" förslag,
          // låt regeln vinna för att bryta felaktiga historiska produktkopplingar.
          if (!sameAsCurrent && isTrustedSuggestion) {
            this.logger.log(
              `Rule-override: "${item.rawName}" ändras från "${nextSuggestion?.path}" till "${byRule.path}"`,
            );
            nextSuggestion = byRule;
            pushTrace(`rule override trusted -> "${byRule.path}"`);
          }
        } else if (!nextSuggestion && byRule) {
          nextSuggestion = byRule;
          pushTrace(`rule fallback applied -> "${byRule.path}"`);
        }

        // AI används som fallback när varken matchning eller regler satte kategori
        if (!nextSuggestion) {
          pushTrace('ai invoked');
          nextSuggestion = await this.aiService.suggestCategory(item.rawName, categories);
          pushTrace(`ai result -> "${nextSuggestion.path}" (${nextSuggestion.confidence})`);
        } else {
          pushTrace(`ai skipped, current -> "${nextSuggestion.path}"`);
        }

        const beforeGuardPath = nextSuggestion?.path;
        const guardedSuggestion = nextSuggestion
          ? this.applyContradictionGuard(signalText || item.rawName, nextSuggestion, categories)
          : null;
        if (guardedSuggestion && beforeGuardPath !== guardedSuggestion.path) {
          pushTrace(`contradiction guard remap "${beforeGuardPath}" -> "${guardedSuggestion.path}"`);
        }

        const beforeHardPath = guardedSuggestion?.path;
        const finalSuggestion = guardedSuggestion
          ? this.applyHardCategoryOverrides(signalText || item.rawName, guardedSuggestion, categories)
          : null;
        if (finalSuggestion && beforeHardPath !== finalSuggestion.path) {
          pushTrace(`hard override remap "${beforeHardPath}" -> "${finalSuggestion.path}"`);
        }

        if (finalSuggestion) {
          pushTrace(`final -> "${finalSuggestion.path}" (${finalSuggestion.confidence})`);
        } else {
          pushTrace('final -> no categorySuggestion');
        }

        if (traceEnabled) {
          this.logger.log(`[ReceiptDecision] ${trace.join(' | ')}`);
        }

        enriched.push(
          finalSuggestion
            ? { ...item, categorySuggestion: finalSuggestion }
            : item,
        );
      } catch (err) {
        const traceSignalText = [
          item.rawName,
          item.matchedProductName,
          item.suggestedProductName,
        ]
          .filter((v): v is string => typeof v === 'string' && v.trim().length > 0)
          .join(' ');
        if (this.shouldTraceDecision(traceSignalText || item.rawName)) {
          this.logger.warn(
            `[ReceiptDecision] error raw="${item.rawName}" signal="${traceSignalText || item.rawName}" err=${String(err)}`,
          );
        }
        // Om AI-anrop misslyckas för enskild vara — hoppa över utan att kasta
        enriched.push(item);
      }
    }

    return enriched;
  }

  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 (
      /\bbacon\b/.test(normalized) ||
      /\bbacn\b/.test(normalized) ||
      /\bbaco\b/.test(normalized) ||
      /\bsidflask\b/.test(normalized) ||
      /\bpancetta\b/.test(normalized)
    );
  }

  private applyHardCategoryOverrides(
    signalText: string,
    suggestion: CategorySuggestion,
    categories: Awaited<ReturnType<CategoriesService['findFlattened']>>,
  ): CategorySuggestion {
    const normalized = normalizeForRules(signalText);
    const hasBaconLikeSignal =
      /\bbacon\b/.test(normalized) ||
      /\bbacn\b/.test(normalized) ||
      /\bbaco\b/.test(normalized) ||
      /\bbac[a-z]{1,3}\b/.test(normalized) ||
      /\bsidflask\b/.test(normalized) ||
      /\bpancetta\b/.test(normalized);

    if (!hasBaconLikeSignal) return suggestion;

    const l3Pork = categories.find(
      (c) =>
        c.name.toLowerCase() === 'fläsk' &&
        c.path.toLowerCase().startsWith('kött, chark & fågel > kött > '),
    );
    if (!l3Pork) 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 =
      /\bbacon\b/.test(normalized) ||
      /\bbacn\b/.test(normalized) ||
      /\bbaco\b/.test(normalized) ||
      /\bsidflask\b/.test(normalized) ||
      /\bpancetta\b/.test(normalized) ||
      /\bflask\b/.test(normalized) ||
      /\bflaskfile\b/.test(normalized) ||
      /\bkarr[eé]\b/.test(normalized) ||
      /\bkotlett\b/.test(normalized);

    if (hasPorkSignal) {
      const l3Pork = findCategory({
        name: 'fläsk',
        startsWith: 'kött, chark & fågel > kött > ',
      });
      const hit = toSuggestion(l3Pork, '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: 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 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 (saknar egen L2/L3 i nuvarande träd) ────────────────
    const hasEggSignal =
      /\bagg\b/.test(normalized) ||
      /\begg\b/.test(normalized) ||
      /\binne\b/.test(normalized) ||
      /\b24p\b/.test(normalized);

    if (hasEggSignal) {
      const l1DairyEgg = categories.find(
        (c) => c.path.toLowerCase() === 'mejeri, ost & ägg',
      );
      if (l1DairyEgg) {
        return {
          categoryId: l1DairyEgg.id,
          categoryName: l1DairyEgg.name,
          path: l1DairyEgg.path,
          confidence: 'high',
          usedFallback: false,
        };
      }
    }

    // ── 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'),
      );
      if (l3Te) {
        return { categoryId: l3Te.id, categoryName: l3Te.name, path: l3Te.path, confidence: 'high', usedFallback: false };
      }
      const l2TeChoklad = categories.find(
        (c) => c.name.toLowerCase() === 'te & choklad' && c.path.toLowerCase().startsWith('dryck'),
      );
      if (l2TeChoklad) {
        return { categoryId: l2TeChoklad.id, categoryName: l2TeChoklad.name, path: l2TeChoklad.path, confidence: 'medium', usedFallback: false };
      }
    }

    // ── Regel: Kaffebröd ─────────────────────────────────────────────────
    const isKaffebrod =
      /\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'),
      );
      if (l3Kaffebrod) {
        return { categoryId: l3Kaffebrod.id, categoryName: l3Kaffebrod.name, path: l3Kaffebrod.path, confidence: 'high', usedFallback: false };
      }
      const l2Kondis = categories.find(
        (c) => c.name.toLowerCase() === 'kondis & fika' && c.path.toLowerCase().startsWith('bröd & kakor'),
      );
      if (l2Kondis) {
        return { categoryId: l2Kondis.id, categoryName: l2Kondis.name, path: l2Kondis.path, confidence: 'medium', usedFallback: false };
      }
    }

    // ── 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 > '),
    );
    if (l3AllergyCooking) {
      return {
        categoryId: l3AllergyCooking.id,
        categoryName: l3AllergyCooking.name,
        path: l3AllergyCooking.path,
        confidence: 'high',
        usedFallback: false,
      };
    }

    const l2Cooking = categories.find(
      (c) =>
        c.name.toLowerCase() === 'matlagning' &&
        c.path.toLowerCase() === 'mejeri, ost & ägg > matlagning',
    );
    if (l2Cooking) {
      return {
        categoryId: l2Cooking.id,
        categoryName: l2Cooking.name,
        path: l2Cooking.path,
        confidence: 'medium',
        usedFallback: false,
      };
    }

    return null;
  }

  private applyContradictionGuard(
    rawName: string,
    suggestion: CategorySuggestion,
    categories: Awaited<ReturnType<CategoriesService['findFlattened']>>,
  ): CategorySuggestion {
    const normalized = normalizeForRules(rawName);
    const hasPorkSignal =
      /\bbacon\b/.test(normalized) ||
      /\bbacn\b/.test(normalized) ||
      /\bbaco\b/.test(normalized) ||
      /\bsidflask\b/.test(normalized) ||
      /\bpancetta\b/.test(normalized) ||
      /\bflask\b/.test(normalized) ||
      /\bflaskfile\b/.test(normalized) ||
      /\bkarr[eé]\b/.test(normalized) ||
      /\bkotlett\b/.test(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 = categories.find(
        (c) =>
          c.name.toLowerCase() === 'fläsk' &&
          c.path.toLowerCase().startsWith('kött, chark & fågel > kött > '),
      );
      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 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;

      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,
      };
    }

    return suggestion;
  }
}
