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

export function isIgnoredReceiptName(value: string | null | undefined): boolean {
  const normalized = (value ?? '').trim().toLowerCase();
  if (!normalized) return false;

  if (/^rabatt\b/.test(normalized)) return true;
  if (/^summa\b/.test(normalized)) return true;
  if (/^moms\b/.test(normalized)) return true;
  if (/^pant\b/.test(normalized)) return true;
  if (/^att\s+betala\b/.test(normalized)) return true;
  if (/^totalt\b/.test(normalized)) return true;
  if (/^kort\b/.test(normalized)) return true;
  if (/^kontant\b/.test(normalized)) return true;
  if (/^willys\s+plus\s*[:\-]?\b/.test(normalized)) return true;

  return false;
}

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

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

function inferPackageDebugFromRawName(rawName: string): {
  packageCount: number;
  packQuantity: number | null;
  packUnit: string | null;
} {
  const normalized = rawName.toLowerCase();

  // e.g. "3x120g", "2 x 1.5l"
  const multiPack = /(\d+)\s*[x×]\s*(\d+(?:[\.,]\d+)?)\s*(ml|cl|dl|l|g|kg)\b/i.exec(normalized);
  if (multiPack) {
    const count = Number.parseInt(multiPack[1], 10);
    const qty = Number.parseFloat(multiPack[2].replace(',', '.'));
    const unit = multiPack[3].toLowerCase();
    return {
      packageCount: Number.isFinite(count) && count > 0 ? count : 1,
      packQuantity: Number.isFinite(qty) ? qty : null,
      packUnit: unit,
    };
  }

  // e.g. "5dl", "1,5l"
  const singlePack = /(\d+(?:[\.,]\d+)?)\s*(ml|cl|dl|l|g|kg)\b/i.exec(normalized.replace(/([\d.,]+)(ml|cl|dl|l|g|kg)\b/i, '$1 $2'));
  if (singlePack) {
    const qty = Number.parseFloat(singlePack[1].replace(',', '.'));
    const unit = singlePack[2].toLowerCase();
    return {
      packageCount: 1,
      packQuantity: Number.isFinite(qty) ? qty : null,
      packUnit: unit,
    };
  }

  return {
    packageCount: 1,
    packQuantity: null,
    packUnit: null,
  };
}

@Injectable()
export class ReceiptImportService {
  private readonly logger = new Logger(ReceiptImportService.name);
  private cachedCategories: any[] = [];

  constructor(
    private readonly prisma: PrismaService,
    private readonly aiService: AiService,
    private readonly categoriesService: CategoriesService,
  ) {
    this.loadCategories();
  }

  async loadCategories() {
    this.cachedCategories = await this.prisma.category.findMany({
      include: { children: true },
    });
  }

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

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

  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
      }
      if (response.status === 503 || response.status === 429) {
        throw new ServiceUnavailableException(message);
      }
      throw new BadRequestException(message);
    }

    const items = (await response.json()) as ParsedReceiptItem[];
    return items.filter((item) => !isIgnoredReceiptName(item.rawName));
  }

  private async matchProducts(
    items: ParsedReceiptItem[],
    userId?: number,
  ): Promise<ParsedReceiptItem[]> {
    type UnitMappingLite = { productId: number; originalUnit: string; preferredUnit: string };
    type AliasLite = {
      receiptName: string;
      product: {
        id: number;
        name: string;
        canonicalName: string | null;
        categoryRef: { id: number; name: string } | null;
      };
    };

    const prismaAny = this.prisma as any;

    // Hämta alias och produkter parallellt — filtrera på userId om angivet
    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[]);

    const [aliases, products, unitMappings] = await Promise.all([
      this.prisma.receiptAlias.findMany({
        where: aliasFilter,
        orderBy: [
          { isGlobal: 'asc' },
          { id: 'asc' },
        ],
        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 } } },
      }),
      unitMappingsPromise,
    ]) as [AliasLite[], { id: number; name: string; canonicalName: string | null; categoryId: number | null; categoryRef: { id: number; name: string } | null }[], UnitMappingLite[]];

    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: AliasLite) => a.receiptName === raw);
      if (alias) {
        const mappedUnit = unitMappings.find(
          (um) =>
            um.productId === alias.product.id &&
            um.originalUnit === (item.unit ?? '').trim().toLowerCase(),
        )?.preferredUnit;
        const cat = alias.product.categoryRef;
        return {
          ...item,
          matchedProductId: alias.product.id,
          matchedProductName: alias.product.canonicalName ?? alias.product.name,
          unit: mappedUnit ?? item.unit,
          matchedVia: 'alias' as const,
          ...(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, matchedVia: 'none' as const };
      }

      // Kontrollera om det finns en enhetsmappning för produkten och användaren
      const unitMapping = unitMappings.find(
        (um) =>
          um.productId === suggestion.id &&
          um.originalUnit === (item.unit ?? '').trim().toLowerCase(),
      );
      const preferredUnit = unitMapping ? unitMapping.preferredUnit : item.unit;

      const cat = suggestion.categoryRef;
      return {
        ...item,
        suggestedProductId: suggestion.id,
        suggestedProductName: suggestion.canonicalName ?? suggestion.name,
        unit: preferredUnit,
        matchedVia: 'wordmatch' as const,
        ...(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[], userId?: number): 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 user = userId
      ? await this.prisma.user.findUnique({
          where: { id: userId },
          select: { aiEngineEnabled: true },
        })
      : null;

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

        const pkg = inferPackageDebugFromRawName(item.rawName);

        pushTrace(
          `start raw="${item.rawName}" signal="${signalText || item.rawName}" parsedQuantity=${item.quantity ?? 'null'} parsedUnit=${item.unit ?? 'null'} packageCount=${pkg.packageCount} packQuantity=${pkg.packQuantity ?? 'null'} packUnit=${pkg.packUnit ?? 'null'}`,
        );
        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 && nextSuggestion && nextSuggestion.confidence !== 'high') {
            nextSuggestion = { ...nextSuggestion, confidence: 'high' };
            pushTrace(`rule applied -> "${byRule.path}" (confidence upgraded to high)`);
          } else 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) {
          if (user?.aiEngineEnabled) {
            pushTrace('ai invoked');
            nextSuggestion = await this.aiService.suggestCategory(item.rawName, categories);
            pushTrace(`ai result -> "${nextSuggestion.path}" (${nextSuggestion.confidence})`);
          } else {
            pushTrace('ai skipped, feature disabled');
          }
        } 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, matchedVia: item.matchedVia ?? (finalSuggestion ? 'ai' as const : 'none' as const) }
            : 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 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: 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',
      );
      if (l2Egg) {
        return {
          categoryId: l2Egg.id,
          categoryName: l2Egg.name,
          path: l2Egg.path,
          confidence: 'high',
          usedFallback: false,
        };
      }

      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: 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'),
      );
      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 =
      /\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'),
      );
      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: 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 > '),
    );
    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 = 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;
  }
}
