import { Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { normalizeName } from '../common/utils/normalize-name';
import { ReceiptMatchItemDto } from './dto/receipt-match.dto';
import { ReceiptMatchPreviewResponse, ReceiptMatchPreviewRow } from './dto/receipt-match.response';

export type CandidateSelection = {
  id: number;
  sessionId: number;
  itemId: number;
  plannedQuantity: Prisma.Decimal | null;
  plannedUnit: string | null;
  item: {
    rawName: string;
    normalizedName: string;
    matchedProductId: number | null;
    matchedProductName: string | null;
  };
};

const AUTO_MATCH_THRESHOLD = 0.9;
const AMBIGUOUS_THRESHOLD = 0.7;

@Injectable()
export class FlyerSelectionMatcherService {
  matchRows(items: ReceiptMatchItemDto[], candidates: CandidateSelection[]): ReceiptMatchPreviewRow[] {
    const remainingSelectionIds = new Set(candidates.map((candidate) => candidate.id));
    const byProductId = new Map<number, CandidateSelection[]>();
    const byNormalizedName = new Map<string, CandidateSelection[]>();

    for (const candidate of candidates) {
      const productId = candidate.item.matchedProductId;
      if (productId != null) {
        const list = byProductId.get(productId) ?? [];
        list.push(candidate);
        byProductId.set(productId, list);
      }
      const normalized = normalizeName(candidate.item.normalizedName || candidate.item.rawName);
      if (normalized) {
        const list = byNormalizedName.get(normalized) ?? [];
        list.push(candidate);
        byNormalizedName.set(normalized, list);
      }
    }

    const rows: ReceiptMatchPreviewRow[] = [];
    for (let rowIndex = 0; rowIndex < items.length; rowIndex++) {
      const receiptItem = items[rowIndex];
      const candidatesForRow = this.candidatePool(receiptItem, candidates, remainingSelectionIds, byProductId, byNormalizedName);
      const best = this.findBest(receiptItem, candidatesForRow);

      if (!best) {
        rows.push({
          rowIndex,
          status: 'unmatched',
          confidence: 0,
          matchedVia: 'none',
          reasonCodes: ['no_match'],
          selectionId: null,
          sessionId: null,
          itemId: null,
          plannedName: null,
          plannedProductId: null,
          plannedProductName: null,
        });
        continue;
      }

      const status =
        best.confidence >= AUTO_MATCH_THRESHOLD
          ? 'auto'
          : best.confidence >= AMBIGUOUS_THRESHOLD
            ? 'ambiguous'
            : 'unmatched';

      if (status !== 'unmatched') {
        remainingSelectionIds.delete(best.candidate.id);
      }

      rows.push({
        rowIndex,
        status,
        confidence: Number(best.confidence.toFixed(3)),
        matchedVia: best.matchedVia,
        reasonCodes: best.reasons,
        selectionId: best.candidate.id,
        sessionId: best.candidate.sessionId,
        itemId: best.candidate.itemId,
        plannedName: best.candidate.item.rawName,
        plannedProductId: best.candidate.item.matchedProductId,
        plannedProductName: best.candidate.item.matchedProductName,
      });
    }

    return rows;
  }

  toPreviewResponse(rows: ReceiptMatchPreviewRow[], candidateSelectionCount: number): ReceiptMatchPreviewResponse {
    const autoCount = rows.filter((row) => row.status === 'auto').length;
    const ambiguousCount = rows.filter((row) => row.status === 'ambiguous').length;
    const unmatchedCount = rows.filter((row) => row.status === 'unmatched').length;
    return { rows, autoCount, ambiguousCount, unmatchedCount, candidateSelectionCount };
  }

  private candidatePool(
    receiptItem: ReceiptMatchItemDto,
    allCandidates: CandidateSelection[],
    remainingSelectionIds: Set<number>,
    byProductId: Map<number, CandidateSelection[]>,
    byNormalizedName: Map<string, CandidateSelection[]>,
  ): CandidateSelection[] {
    const pool = new Set<CandidateSelection>();

    if (receiptItem.productId != null) {
      for (const candidate of byProductId.get(receiptItem.productId) ?? []) {
        if (remainingSelectionIds.has(candidate.id)) pool.add(candidate);
      }
    }

    const receiptNormalized = normalizeName(receiptItem.normalizedName ?? receiptItem.rawName);
    if (receiptNormalized) {
      for (const candidate of byNormalizedName.get(receiptNormalized) ?? []) {
        if (remainingSelectionIds.has(candidate.id)) pool.add(candidate);
      }
    }

    if (pool.size > 0) {
      return [...pool];
    }

    return allCandidates.filter((candidate) => remainingSelectionIds.has(candidate.id));
  }

  private findBest(
    receiptItem: ReceiptMatchItemDto,
    candidates: CandidateSelection[],
  ): { candidate: CandidateSelection; confidence: number; matchedVia: string; reasons: string[] } | null {
    let best: { candidate: CandidateSelection; confidence: number; matchedVia: string; reasons: string[] } | null = null;
    for (const candidate of candidates) {
      const evaluated = this.scoreCandidate(receiptItem, candidate);
      if (evaluated.confidence <= 0) continue;
      if (!best || evaluated.confidence > best.confidence) best = evaluated;
    }
    return best;
  }

  private scoreCandidate(receiptItem: ReceiptMatchItemDto, candidate: CandidateSelection) {
    const reasons: string[] = [];
    const receiptNormalized = normalizeName(receiptItem.normalizedName ?? receiptItem.rawName);
    const flyerNormalized = normalizeName(candidate.item.normalizedName || candidate.item.rawName);

    if (receiptItem.productId && candidate.item.matchedProductId === receiptItem.productId) {
      reasons.push('product_id_exact');
      return { candidate, confidence: 1, matchedVia: 'product_id', reasons };
    }

    if (receiptNormalized && flyerNormalized && receiptNormalized === flyerNormalized) {
      let confidence = 0.93;
      reasons.push('name_exact');
      confidence += this.quantityUnitBonus(receiptItem, candidate, reasons);
      return { candidate, confidence: Math.min(0.99, confidence), matchedVia: 'name_exact', reasons };
    }

    const overlap = this.tokenOverlap(this.tokenize(receiptItem.rawName), this.tokenize(candidate.item.rawName));
    if (overlap <= 0) {
      return { candidate, confidence: 0, matchedVia: 'none', reasons: ['no_token_overlap'] };
    }

    let confidence = Math.min(0.89, 0.45 + overlap * 0.45);
    reasons.push(`token_overlap:${overlap.toFixed(2)}`);
    confidence += this.quantityUnitBonus(receiptItem, candidate, reasons);
    confidence = Math.min(0.89, confidence);
    return { candidate, confidence, matchedVia: 'token', reasons };
  }

  private quantityUnitBonus(receiptItem: ReceiptMatchItemDto, candidate: CandidateSelection, reasons: string[]): number {
    let bonus = 0;
    if (receiptItem.quantity != null && candidate.plannedQuantity != null && Number(candidate.plannedQuantity) === receiptItem.quantity) {
      bonus += 0.03;
      reasons.push('quantity_match');
    }
    const receiptUnit = (receiptItem.unit ?? '').trim().toLowerCase();
    const plannedUnit = (candidate.plannedUnit ?? '').trim().toLowerCase();
    if (receiptUnit && plannedUnit && receiptUnit === plannedUnit) {
      bonus += 0.03;
      reasons.push('unit_match');
    }
    return bonus;
  }

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

  private tokenOverlap(a: string[], b: string[]): number {
    if (a.length === 0 || b.length === 0) return 0;
    const as = new Set(a);
    const bs = new Set(b);
    let intersection = 0;
    for (const token of as) {
      if (bs.has(token)) intersection++;
    }
    const union = new Set([...as, ...bs]).size;
    if (union === 0) return 0;
    return intersection / union;
  }
}
