/**
 * Legacy OpenCart catalog import (F-114).
 *
 * This module is a pure helper that:
 *  - declares the legacy categories from `oc_category_description`
 *  - normalises names (HTML entities, whitespace, case)
 *  - classifies each entry as a real category, a brand, or excluded
 *  - picks a parent category for each new category
 *
 * The seed script (`scripts/seed-legacy-categories.mjs`) consumes this helper
 * to import the entries idempotently. Domain logic is kept here so it can be
 * unit tested without touching the database.
 */

export type LegacyKind = 'category' | 'brand' | 'exclude';

/**
 * A parent assignment. We resolve by `slug` against the existing categories
 * at runtime so the seed script can look up the UUID once, and so the
 * mapping is easy to read in tests.
 */
export interface LegacyEntry {
  /** Original OpenCart category_id. */
  ocId: number;
  /** Decoded, trimmed name. */
  name: string;
  /** Classification (category / brand / exclude). */
  kind: LegacyKind;
  /** Parent slug for new categories. undefined = root. */
  parentSlug?: string;
}

/** Lowercases + collapses whitespace + decodes HTML entities. */
export function normalizeName(raw: string): string {
  const decoded = decodeHtmlEntities(raw)
    .replace(/\s+/g, ' ')
    .trim();
  return decoded;
}

/** Case-insensitive comparison for "already exists" checks. */
export function isSameName(left: string, right: string): boolean {
  return normalizeName(left).toLowerCase() === normalizeName(right).toLowerCase();
}

/** Minimal HTML entity decoder for the entities we know appear in the legacy table. */
export function decodeHtmlEntities(raw: string): string {
  const named: Record<string, string> = {
    amp: '&',
    lt: '<',
    gt: '>',
    quot: '"',
    apos: "'",
    nbsp: ' ',
  };
  return raw.replace(/&([a-zA-Z]+|#\d+|#x[0-9a-fA-F]+);/g, (entity, body) => {
    if (body in named) return named[body] ?? entity;
    if (body.startsWith('#x') || body.startsWith('#X')) {
      const code = parseInt(body.slice(2), 16);
      if (Number.isFinite(code)) return String.fromCodePoint(code);
    } else if (body.startsWith('#')) {
      const code = parseInt(body.slice(1), 10);
      if (Number.isFinite(code)) return String.fromCodePoint(code);
    }
    return entity;
  });
}

/**
 * Converts a name into a URL slug matching the categories/brands validator:
 * `^[a-z0-9]+(?:-[a-z0-9]+)*$`. Strips diacritics, drops non-alphanumerics.
 */
export function slugify(name: string): string {
  const stripped = normalizeName(name)
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '')
    .toLowerCase()
    .replace(/&/g, ' and ')
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-+|-+$/g, '');
  return stripped || 'entry';
}

/**
 * Returns a slug that does not collide with `existing` (case-insensitive).
 * Appends `-2`, `-3`, … until a free one is found.
 */
export function uniqueSlug(base: string, existing: ReadonlySet<string>): string {
  const lower = base.toLowerCase();
  if (!existing.has(lower)) return base;
  let i = 2;
  while (existing.has(`${lower}-${i}`)) i += 1;
  return `${base}-${i}`;
}

/**
 * The legacy OpenCart rows. Each entry is classified and (for categories)
 * pinned to a parent by slug so the import is deterministic.
 * Excluded entries are legacy markers (`00 - SIN CODIGO`, etc.) that we
 * do not want to surface in the new catalog.
 */
export const LEGACY_ENTRIES: ReadonlyArray<LegacyEntry> = [
  { ocId: 66, name: '00 - SIN CODIGO', kind: 'exclude' },
  { ocId: 67, name: 'HERBALIST', kind: 'category', parentSlug: 'hierbas-infusiones' },
  { ocId: 68, name: 'FOOD', kind: 'category', parentSlug: 'alimentacion' },
  { ocId: 69, name: 'COSMETICS', kind: 'category', parentSlug: 'cosmetica-e-higiene' },
  { ocId: 70, name: 'DIET AND NUTRITION', kind: 'category', parentSlug: 'suplementos' },
  { ocId: 71, name: 'NUTS & SEEDS', kind: 'category', parentSlug: 'frutos-secos' },
  { ocId: 72, name: 'BREAD & PASTRIES', kind: 'category', parentSlug: 'alimentacion' },
  { ocId: 75, name: 'LEGUMES', kind: 'category', parentSlug: 'alimentacion' },
  { ocId: 76, name: 'FLOUR & CEREALS', kind: 'category', parentSlug: 'alimentacion' },
  { ocId: 77, name: 'PASTA & RICE', kind: 'category', parentSlug: 'alimentacion' },
  { ocId: 78, name: 'CREAMS & JAMS', kind: 'category', parentSlug: 'cremas' },
  { ocId: 79, name: 'FRUTAS Y VERDURAS', kind: 'category', parentSlug: 'alimentacion' },
  { ocId: 80, name: 'SUGAR & SWEETENERS', kind: 'category', parentSlug: 'alimentacion' },
  { ocId: 81, name: 'BEVERAGES', kind: 'category', parentSlug: 'alimentacion' },
  { ocId: 82, name: 'OIL AND VINEGAR', kind: 'category', parentSlug: 'aceites' },
  { ocId: 83, name: 'RAW FOOD', kind: 'category', parentSlug: 'alimentacion' },
  { ocId: 84, name: 'HIERBAS MEDICINALES', kind: 'category', parentSlug: 'hierbas-infusiones' },
  { ocId: 85, name: 'SUPLEMENTS', kind: 'category', parentSlug: 'suplementos' },
  { ocId: 86, name: 'OILS & EXTRACTS', kind: 'category', parentSlug: 'aceites' },
  { ocId: 87, name: 'FACIAL', kind: 'category', parentSlug: 'cosmetica-e-higiene' },
  { ocId: 88, name: 'CORPORAL', kind: 'category', parentSlug: 'cosmetica-e-higiene' },
  { ocId: 89, name: 'ASEO PERSONAL', kind: 'category', parentSlug: 'cosmetica-e-higiene' },
  { ocId: 90, name: 'HOME', kind: 'category', parentSlug: 'hogar-y-mascotas' },
  { ocId: 91, name: 'MACROBIOTIC', kind: 'category', parentSlug: 'alimentacion' },
  { ocId: 93, name: 'SNACKS', kind: 'category', parentSlug: 'alimentacion' },
  { ocId: 95, name: 'BOOKS', kind: 'category', parentSlug: 'hogar-y-mascotas' },
  { ocId: 97, name: 'FRESH PRODUCTS', kind: 'category', parentSlug: 'alimentacion' },
  { ocId: 102, name: 'WINE', kind: 'category', parentSlug: 'alimentacion' },
  { ocId: 103, name: 'BEER', kind: 'category', parentSlug: 'alimentacion' },
  { ocId: 104, name: 'VEGETAL MILKS', kind: 'category', parentSlug: 'alimentacion' },
  { ocId: 105, name: 'JUICES', kind: 'category', parentSlug: 'alimentacion' },
  { ocId: 106, name: 'SODAS', kind: 'category', parentSlug: 'alimentacion' },
  { ocId: 107, name: 'CHILDREN', kind: 'category', parentSlug: 'alimentacion' },
  { ocId: 108, name: 'BABYS & KIDS', kind: 'category', parentSlug: 'cosmetica-e-higiene' },
  { ocId: 115, name: 'TEA & INFUSIONS', kind: 'category', parentSlug: 'hierbas-infusiones' },
  { ocId: 116, name: 'SPICE & CONDIMENTS', kind: 'category', parentSlug: 'alimentacion' },
  { ocId: 117, name: 'CHOCOLATE & SWEETS', kind: 'category', parentSlug: 'alimentacion' },
  { ocId: 129, name: 'CLEANING', kind: 'category', parentSlug: 'limpieza-ecologica' },
  { ocId: 127, name: 'PROVEEDORES', kind: 'category' }, // root
  // ── Marcas ───────────────────────────────────────────────────────────────
  { ocId: 98, name: 'PROVEEDORES', kind: 'brand' },
  { ocId: 99, name: 'BIOSPIRIT', kind: 'brand' },
  { ocId: 100, name: 'SALUD VIVA', kind: 'brand' },
  { ocId: 101, name: 'GLOBO NATURA', kind: 'brand' },
  { ocId: 102, name: 'SOLGAR', kind: 'brand' },
  { ocId: 103, name: 'VITAFOOD', kind: 'brand' },
  { ocId: 104, name: 'EL GRANERO INTEGRAL', kind: 'brand' },
  { ocId: 105, name: 'A.VOGEL', kind: 'brand' },
  { ocId: 106, name: 'BIOCOP', kind: 'brand' },
  { ocId: 107, name: 'QBIO', kind: 'brand' },
  { ocId: 108, name: 'LA FINESTRA SUL CIELO', kind: 'brand' },
  { ocId: 109, name: 'CADIDIET', kind: 'brand' },
  { ocId: 110, name: 'COMPLEMENTOS Y NUTRICIÓN', kind: 'brand' },
  { ocId: 111, name: 'CHISVERT', kind: 'brand' },
  { ocId: 112, name: 'SUPERFOOD', kind: 'brand' },
  { ocId: 113, name: 'DIETISUR', kind: 'brand' },
  { ocId: 114, name: 'GOURMET BIO', kind: 'brand' },
  { ocId: 116, name: 'NATURALMENTE MEDITERRANEO', kind: 'brand' },
  { ocId: 117, name: 'NATURAL', kind: 'brand' },
  { ocId: 126, name: 'GOURMET CASH', kind: 'brand' },
  { ocId: 127, name: 'NUTRINAT', kind: 'brand' },
  { ocId: 131, name: 'COMERCIAL GARZA', kind: 'brand' },
  { ocId: 132, name: 'ARTESANÍA AGRÍCOLA', kind: 'brand' },
  { ocId: 133, name: 'DAS BROT', kind: 'brand' },
  { ocId: 134, name: 'DULCES LISSEN', kind: 'brand' },
  { ocId: 135, name: 'LEMONPHARMA', kind: 'brand' },
  { ocId: 136, name: 'VEKINE', kind: 'brand' },
  { ocId: 137, name: 'NAAY BOTANICALS', kind: 'brand' },
  { ocId: 138, name: 'LAMBERTS', kind: 'brand' },
  { ocId: 139, name: 'YODETIENDAS', kind: 'brand' },
  { ocId: 140, name: 'NATURCOSMETIKA', kind: 'brand' },
  { ocId: 142, name: 'TONGIL', kind: 'brand' },
  { ocId: 96, name: 'SUPLEMENTS', kind: 'exclude' }, // duplicate of 85 (we keep 85 as category)
  { ocId: 141, name: '01 - PRODUCTOS DESCATALOGADOS', kind: 'exclude' },
  { ocId: 143, name: '02 - PRODUCTOS RAPIDOS', kind: 'exclude' },
  { ocId: 145, name: 'GRANOLA', kind: 'category', parentSlug: 'alimentacion' },
];

export interface ImportPlanCategory {
  ocId: number;
  name: string;
  slug: string;
  parentSlug?: string;
}

export interface ImportPlanBrand {
  ocId: number;
  name: string;
  slug: string;
}

export interface ImportPlan {
  categories: ImportPlanCategory[];
  brands: ImportPlanBrand[];
}

/**
 * F-116: Spanish translations of every legacy category name in Title Case.
 * Names already in Spanish are kept as-is. Keys are the `name` field of each
 * legacy entry (case-insensitive matching is done by the caller).
 */
export const CATEGORY_TRANSLATIONS: Readonly<Record<string, string>> = {
  HERBALIST: 'Herbolaria',
  FOOD: 'Alimentación',
  COSMETICS: 'Cosmética',
  'DIET AND NUTRITION': 'Dietética y Nutrición',
  'NUTS & SEEDS': 'Frutos Secos y Semillas',
  'BREAD & PASTRIES': 'Pan y Bollería',
  LEGUMES: 'Legumbres',
  'FLOUR & CEREALS': 'Harinas y Cereales',
  'PASTA & RICE': 'Pasta y Arroz',
  'CREAMS & JAMS': 'Cremas y Mermeladas',
  'SUGAR & SWEETENERS': 'Azúcar y Endulzantes',
  BEVERAGES: 'Bebidas',
  'OIL AND VINEGAR': 'Aceite y Vinagre',
  'RAW FOOD': 'Comida Cruda',
  'SUPLEMENTS': 'Suplementos',
  'OILS & EXTRACTS': 'Aceites y Extractos',
  FACIAL: 'Facial',
  CORPORAL: 'Corporal',
  'ASEO PERSONAL': 'Aseo Personal',
  HOME: 'Hogar',
  MACROBIOTIC: 'Macrobiótica',
  SNACKS: 'Snacks',
  BOOKS: 'Libros',
  'FRESH PRODUCTS': 'Productos Frescos',
  WINE: 'Vino',
  BEER: 'Cerveza',
  'VEGETAL MILKS': 'Bebidas Vegetales',
  JUICES: 'Zumos',
  SODAS: 'Refrescos',
  CHILDREN: 'Niños',
  'BABYS & KIDS': 'Bebés y Niños',
  'TEA & INFUSIONS': 'Té e Infusiones',
  'SPICE & CONDIMENTS': 'Especias y Condimentos',
  'CHOCOLATE & SWEETS': 'Chocolate y Dulces',
  CLEANING: 'Limpieza',
  // Aliases already in Spanish (kept for completeness).
  'FRUTAS Y VERDURAS': 'Frutas y Verduras',
  'HIERBAS MEDICINALES': 'Hierbas Medicinales',
  PROVEEDORES: 'Proveedores',
  GRANOLA: 'Granola',
};

export function translateCategoryName(name: string): string {
  const exact = CATEGORY_TRANSLATIONS[name];
  if (exact) return exact;
  const upper = name.toUpperCase();
  return CATEGORY_TRANSLATIONS[upper] ?? name;
}

export interface RenamePlanEntry {
  ocId: number;
  oldName: string;
  newName: string;
}

/**
 * F-116: returns the legacy categories whose English name is already in
 * the catalog and need to be renamed to their Spanish translation.
 * Entries whose Spanish translation already exists are skipped (no
 * overwrite).
 */
export function buildRenamePlan(
  existingCategoryNames: ReadonlySet<string>,
): RenamePlanEntry[] {
  const renames: RenamePlanEntry[] = [];
  for (const entry of LEGACY_ENTRIES) {
    if (entry.kind !== 'category') continue;
    const translated = translateCategoryName(entry.name);
    const oldLower = entry.name.toLowerCase();
    // The legacy Spanish entries (FRUTAS Y VERDURAS, SNACKS, GRANOLA) are
    // already on the catalog but with the wrong casing. We rename them.
    if (!existingCategoryNames.has(oldLower)) continue;
    if (entry.name === translated) continue;
    renames.push({ ocId: entry.ocId, oldName: entry.name, newName: translated });
  }
  return renames;
}

/**
 * Builds the import plan from `LEGACY_ENTRIES`, skipping entries whose
 * name (case-insensitive) is already present in `existingCategoryNames` or
 * `existingBrandNames`. Slugs are uniqued against `existingSlugs`.
 */
export function buildImportPlan(
  existingCategoryNames: ReadonlySet<string>,
  existingBrandNames: ReadonlySet<string>,
  existingSlugs: ReadonlySet<string>,
): ImportPlan {
  const categories: ImportPlanCategory[] = [];
  const brands: ImportPlanBrand[] = [];
  const usedSlugs = new Set<string>(existingSlugs);

  for (const entry of LEGACY_ENTRIES) {
    if (entry.kind === 'exclude') continue;
    if (entry.kind === 'category') {
      const translatedName = translateCategoryName(entry.name);
      // Skip if the Spanish name already exists, or if the English name is
      // already in the catalog (rename plan will handle it in place).
      if (existingCategoryNames.has(translatedName.toLowerCase())) continue;
      if (existingCategoryNames.has(entry.name.toLowerCase())) continue;
      const base = slugify(translatedName);
      const slug = uniqueSlug(base, usedSlugs);
      usedSlugs.add(slug.toLowerCase());
      categories.push({
        ocId: entry.ocId,
        name: translatedName,
        slug,
        parentSlug: entry.parentSlug,
      });
    } else if (entry.kind === 'brand') {
      if (existingBrandNames.has(entry.name.toLowerCase())) continue;
      const base = slugify(entry.name);
      const slug = uniqueSlug(base, usedSlugs);
      usedSlugs.add(slug.toLowerCase());
      brands.push({ ocId: entry.ocId, name: entry.name, slug });
    }
  }
  return { categories, brands };
}
