// @ts-nocheck
import { executeEndpointWrite, queryEndpoint } from "../endpoint/endpoint-client.ts";
import { saveScrapePictureCandidate } from "../db/scrape-image-store.ts";
import { openInBrowser, scrapePage, searchInBrowser } from "./browser-adapter.ts";
import {
  prepareManufacturerCatalogSqls,
  mappingSourcesSql,
  productByEanSql,
  productCombinationInfoSql,
  productCombinationsByReferenceSql,
  productToDoByManufacturerSql,
  suggestedProductsSql,
} from "../sql/catalog-maker.ts";

export async function loadManufacturerCatalogState(config, { manufacturerId, position = 1 }) {
  const refreshResults = [];
  if (config.database?.mode === "local" && config.database.allowLocalWrites) {
    for (const sql of prepareManufacturerCatalogSqls({ manufacturerId })) {
      refreshResults.push(
        await executeEndpointWrite(config, sql, {
          operation: "catalog-maker-refresh-manufacturer",
        }),
      );
    }
  }

  const productResult = await queryEndpoint(
    config,
    productToDoByManufacturerSql({ manufacturerId, position }),
  );
  const product = productResult.items[0] ?? null;

  if (!product) {
    return {
      configured: productResult.configured,
      product: null,
      combinations: [],
      message: "No product to do for selected manufacturer.",
    };
  }

  const combinationsResult = await queryEndpoint(
    config,
    productCombinationsByReferenceSql({
      manufacturerId,
      supplierReference: product.supplier_reference,
    }),
  );

  return {
    configured: true,
    refresh: {
      operation: "catalog-maker-refresh-manufacturer",
      statements: refreshResults.length,
      skipped: refreshResults.length === 0,
    },
    product: normalizeProduct(product),
    combinations: combinationsResult.items.map(normalizeCombination),
  };
}

export async function loadProductInfo(config, { productId, manufacturerId, supplierReference }) {
  if (!productId) {
    return {
      configured: true,
      items: [],
      message: "Product info needs id_product first.",
    };
  }

  const result = await queryEndpoint(
    config,
    productCombinationInfoSql({ productId, manufacturerId, supplierReference }),
  );

  return {
    configured: result.configured,
    items: result.items.map(normalizeProductInfo),
  };
}

export async function loadProductByEan(config, { manufacturerId, ean }) {
  const productResult = await queryEndpoint(config, productByEanSql({ manufacturerId, ean }));
  const product = productResult.items[0] ?? null;

  if (!product) {
    return {
      configured: productResult.configured,
      product: null,
      combinations: [],
      message: "EAN not found in product catalog for selected manufacturer.",
    };
  }

  const combinationsResult = await queryEndpoint(
    config,
    productCombinationsByReferenceSql({
      manufacturerId,
      supplierReference: product.supplier_reference,
    }),
  );

  return {
    configured: true,
    product: normalizeProduct(product),
    combinations: combinationsResult.items.map(normalizeCombination),
  };
}

export async function listSuggestedProducts(
  config,
  { manufacturerId, supplierReference, productName },
) {
  const result = await queryEndpoint(
    config,
    suggestedProductsSql({ manufacturerId, supplierReference, productName }),
  );

  return {
    configured: result.configured,
    items: result.items.map((row) => ({
      idProduct: row.id_product,
      name: row.name,
      supplierReference: row.supplier_reference,
      active: row.active,
      imageUrl: row.image_url,
    })),
  };
}

export async function listMappingSources(config, { manufacturerId, checkUrls = false }) {
  try {
    const result = await queryEndpoint(
      config,
      mappingSourcesSql({
        manufacturerId,
        scrapeDatabase: config.database?.scrapeName || "catalog_scrape",
      }),
    );
    const mappingTableName = `${config.database?.scrapeName || "catalog_scrape"}.ps_product_catalog_manufacturer_source`;
    const items = result.items.map((row) =>
      normalizeMappingSource(row, mappingTableName),
    );
    const checkedItems = checkUrls ? await checkMappingSourceUrls(items) : items;

    return {
      configured: result.configured,
      mapped: checkedItems.some((item) => item.enabled),
      tableName: mappingTableName,
      items: checkedItems,
      sourceCount: checkedItems.filter((item) => item.enabled).length,
      testedCount: checkedItems.filter((item) => item.enabled && item.health === "ok").length,
      message: checkedItems.some((item) => item.enabled) ? "" : "Mapping missing",
    };
  } catch (error) {
    if (isMissingMappingSchemaError(error)) {
      return {
        configured: true,
        mapped: false,
        items: [],
        message: "Catalog source tables are missing or have an unexpected structure.",
      };
    }

    throw error;
  }
}

export async function findProductSources(
  config,
  { manufacturerId, sourceKey, productName, supplierReference, ean, eanGroup, color },
) {
  if (!productName && !supplierReference && !ean) {
    throw new Error("Product name, supplier reference, or EAN is required.");
  }

  const mapping = await listMappingSources(config, { manufacturerId });
  const wantedSourceKey = String(sourceKey ?? "").trim().toLowerCase();
  const enabledSources = mapping.items.filter(
    (item) =>
      item.enabled &&
      item.url &&
      (!wantedSourceKey || String(item.key ?? "").toLowerCase() === wantedSourceKey),
  );
  const items = await Promise.all(
    enabledSources.map((source) =>
      findSourceCandidates(source, { productName, supplierReference, ean, eanGroup, color }),
    ),
  );

  return {
    configured: mapping.configured,
    items,
    message: items.length ? "" : "No mapping sources found.",
  };
}

export async function getProductPictures(
  config,
  { manufacturerId, sourceKey, productName, supplierReference, ean, eanGroup, color },
) {
  if (!productName && !supplierReference && !ean) {
    throw new Error("Product name, supplier reference, or EAN is required.");
  }

  const mapping = await listMappingSources(config, { manufacturerId });
  const wantedSourceKey = String(sourceKey ?? "").trim().toLowerCase();
  const sources = mapping.items.filter(
    (item) =>
      item.enabled &&
      item.url &&
      (!wantedSourceKey || String(item.key ?? "").toLowerCase() === wantedSourceKey),
  );
  const items = await Promise.all(
    sources.map((source) =>
      findSourceCandidates(source, { productName, supplierReference, ean, eanGroup, color }, { includePictures: true }),
    ),
  );

  return {
    configured: mapping.configured,
    items: items.map((item) => ({
      source: item.source,
      status: item.status,
      error: item.error || "",
      query: item.query,
      searchUrl: item.searchUrl,
      candidates: item.candidates.map((candidate) => ({
        title: candidate.title,
        rawTitle: candidate.rawTitle,
        url: candidate.url,
        pictures: candidate.pictures || [],
      })),
    })),
    message: items.length ? "" : "No picture sources mapped.",
  };
}

export async function saveProductPictureCandidate(config, picture) {
  return saveScrapePictureCandidate(config, picture);
}

export async function openProductSource(config, { manufacturerId, sourceKey, url }) {
  const sourceUrl = normalizeSourceUrl(url);
  const mapping = await listMappingSources(config, { manufacturerId });
  const source = mapping.items.find(
    (item) => item.enabled && String(item.key ?? "").toLowerCase() === String(sourceKey ?? "").toLowerCase(),
  );

  if (!source) {
    throw new Error("Source mapping was not found for selected manufacturer.");
  }

  await openInBrowser(sourceUrl, { engine: source.browserEngine });

  return {
    configured: mapping.configured,
    opened: true,
    source: {
      key: source.key,
      name: source.name,
      browserEngine: source.browserEngine,
    },
    url: sourceUrl,
  };
}

async function findSourceCandidates(source, product, { includePictures = false } = {}) {
  const searchAttempts = buildAdapterSearchUrls(source, product);
  let lastSearchUrl = searchAttempts[0]?.url || normalizeSourceUrl(source.url);
  let lastError = "";

  if (source.browserEngine === "firefox") {
    const attempt = searchAttempts[0];
    if (!attempt) {
      return {
        source,
        query: "",
        searchUrl: lastSearchUrl,
        status: "error",
        error: "EAN is required for Idealo Playwright search.",
        candidates: [],
      };
    }

    try {
        const result = await searchInBrowser({
          url: attempt.url,
          engine: source.browserEngine,
          headless: true,
        waitAfterLoadMs: source.waitAfterLoadMs,
      });

      if (result.ok) {
        const candidate = formatFoundCandidate(
          { title: result.title || "Idealo result", url: result.url || attempt.url },
          source,
          attempt,
          product,
        );
        if (includePictures) {
          candidate.pictures = source.key === "idealo"
            ? extractIdealoGalleryPictureUrls(result.pageSource, result.url || attempt.url)
            : extractPictureUrls(result.pageSource, result.url || attempt.url);
        }
        return {
          source,
          query: attempt.query,
          searchUrl: result.url || attempt.url,
          status: "found",
          candidates: [candidate],
        };
      }

      return {
        source,
        query: attempt.query,
        searchUrl: result.url || attempt.url,
        status: "error",
        error: "Idealo returned an error page in Playwright.",
        candidates: buildManualSearchCandidates(searchAttempts, product),
      };
    } catch (error) {
      return {
        source,
        query: attempt.query,
        searchUrl: attempt.url,
        status: "manual-search",
        error: `Playwright automation unavailable: ${error.message || "search failed"}`,
        candidates: buildManualSearchCandidates(searchAttempts, product),
      };
    }
  }

  for (const attempt of searchAttempts) {
    lastSearchUrl = attempt.url;
    try {
      const response = await scrapePage(attempt.url, {
        engine: source.browserEngine,
        headless: true,
        waitAfterLoadMs: source.waitAfterLoadMs,
      });
      const html = response.pageSource;
      const candidates = [
          ...extractResponseUrlCandidate(response.url || attempt.url, html, product),
        ...extractDirectCandidates(html, source, product),
      ].slice(0, 3);

      if (includePictures && source.key === "hudy" && product.color) {
        const colorVariantUrl = findColorVariantUrl(html, response.url || attempt.url, product.color);
        if (colorVariantUrl && candidates.length) {
          candidates[0] = { ...candidates[0], url: colorVariantUrl };
        }
      }

      if (candidates.length) {
        let verification = null;
        if (includePictures && source.key === "hudy" && product.color && candidates[0].url) {
          verification = await verifyHudyVariant(candidates[0].url, product, source.browserEngine);
          candidates[0].verification = verification;
          if (verification.matchedUrl) candidates[0].url = verification.matchedUrl;
          if (includePictures && verification.pictures?.length) {
            candidates[0].pictures = verification.pictures;
          }
          candidates[0].title = `${candidates[0].title} / ${verification.verified ? "verified" : "NOT VERIFIED"}`;
        }
        if (includePictures) {
          for (const candidate of candidates) {
            if (source.key === "hudy" && candidate.url) {
              candidate.pictures = await extractPicturesFromPage(candidate.url, source.browserEngine);
            } else if (source.key === "idealo") {
              candidate.pictures = extractIdealoGalleryPictureUrls(html, response.url || attempt.url);
            } else {
              candidate.pictures = extractPictureUrls(html, response.url || attempt.url);
            }
          }
        }
        return {
          source,
          query: attempt.query,
          searchUrl: response.url || attempt.url,
          status: verification && !verification.verified ? "error" : "found",
          ...(verification && !verification.verified
            ? { error: "Hudy page does not match the requested color/EAN group." }
            : {}),
          candidates: candidates.map((candidate) =>
            formatFoundCandidate(candidate, source, attempt, product),
          ),
        };
      }
    } catch (error) {
      lastError = error.message || "Source search failed.";
    }
  }

  return {
    source,
    query: searchAttempts[0]?.query || "",
    searchUrl: searchAttempts[0]?.url || lastSearchUrl,
    status: lastError ? "error" : "manual-search",
    error: lastError,
    candidates: buildManualSearchCandidates(searchAttempts, product),
  };
}

function buildManualSearchCandidates(searchAttempts, product) {
  const candidates = [];
  const seen = new Set();

  for (const attempt of searchAttempts) {
    if (attempt.method !== "GET") continue;
    const key = `${attempt.kind}:${attempt.query}`;
    if (seen.has(key)) continue;
    seen.add(key);
    candidates.push({
      title: formatSearchCandidateTitle(attempt, product),
      url: attempt.url,
    });
    if (candidates.length >= 4) break;
  }

  return candidates;
}

function formatFoundCandidate(candidate, source, attempt, product) {
  const via = source.key ? `Direct ${formatSourceName(source)}` : "Direct source";
  const color = attempt.kind === "EAN" && product.color ? ` / Color: ${product.color}` : "";

  return {
    ...candidate,
    rawTitle: candidate.title,
    title: `${via} / ${attempt.kind}: ${attempt.query}${color}`,
  };
}

function formatSourceName(source) {
  if (source.key === "hudy") return "Hudy";
  if (source.key === "lasportiva") return "La Sportiva";
  if (source.key === "idealo") return "Idealo";
  return source.name || source.key || "source";
}

function formatSearchCandidateTitle(attempt, product) {
  const color = product.color ? ` / Color: ${product.color}` : "";
  if (attempt.kind === "EAN") return `Search EAN: ${attempt.query}${color}`;
  if (attempt.fallbackSourceType === "google_site") return `Google site search: ${attempt.query}`;
  if (attempt.fallbackSourceType === "same_site") return `Same site fallback: ${attempt.query}`;
  if (attempt.fallbackSourceType === "none") return `Fallback disabled: ${attempt.query}`;
  return `Search ${attempt.kind}: ${attempt.query}`;
}

function buildAdapterSearchUrls(source, product) {
  const configuredAttempts = buildConfiguredSearchAttempts(source, product);
  if (configuredAttempts.length) return configuredAttempts;

  const baseUrl = normalizeSourceUrl(source.url);
  const host = getHostname(baseUrl);
  const terms = buildSearchTerms(product);
  const base = baseUrl.replace(/\/$/, "");

  if (host.includes("hudy.cz")) {
    return buildHudySearchAttempts(base, product);
  }

  if (host.includes("lasportiva.com")) {
    return terms.map((term) =>
      buildSearchAttempt(`${base}/catalogsearch/result/?q=${encodeURIComponent(term)}`, term),
    );
  }

  if (host.includes("idealo.de")) {
    return terms.map((term) =>
      buildSearchAttempt(
        `${base}/preisvergleich/MainSearchProductCategory.html?q=${encodeURIComponent(term)}`,
        term,
      ),
    );
  }

  return terms.map((term) => buildSearchAttempt(`${base}/search?q=${encodeURIComponent(term)}`, term));
}

function buildConfiguredSearchAttempts(source, product) {
  const attempts = [];
  const primaryKey = source.searchPrimaryKey || "ean";

  attempts.push(...buildAttemptsFromTemplate(source.searchUrlTemplate, source, product, primaryKey));

  if (source.fallbackSourceType !== "none") {
    for (const key of source.searchFallbackKeys) {
      attempts.push(
        ...buildAttemptsFromTemplate(
          source.fallbackUrlTemplate || source.searchUrlTemplate,
          source,
          product,
          key,
          { fallbackSourceType: source.fallbackSourceType || "same_site" },
        ),
      );
    }
  }

  return dedupeAttempts(attempts);
}

function buildAttemptsFromTemplate(template, source, product, key, options = {}) {
  if (!template) return [];
  const values = getSearchValuesForKey(product, key);

  return values.map((value) =>
    buildSearchAttempt(renderUrlTemplate(template, source, product, { key, value }), value, options),
  );
}

function renderUrlTemplate(template, source, product, { key, value }) {
  const replacements = {
    base_url: normalizeSourceUrl(source.url).replace(/\/$/, ""),
    ean: product.ean || "",
    supplier_reference: product.supplierReference || "",
    product_name: expandProductNames(product.productName)[0] || "",
    product_name_alt: expandProductNames(product.productName)[1] || "",
    color: product.color || "",
    search_value: value || "",
    [key]: value || "",
  };

  return String(template).replace(/\{([a-z0-9_]+)\}/gi, (_, name) =>
    encodeURIComponent(replacements[name] ?? ""),
  );
}

function getSearchValuesForKey(product, key) {
  if (key === "ean") return product.ean ? [product.ean] : [];
  if (key === "supplier_reference") return product.supplierReference ? [product.supplierReference] : [];
  if (key === "product_name") return expandProductNames(product.productName);
  if (key === "color") return product.color ? [product.color] : [];
  return [];
}

function dedupeAttempts(attempts) {
  const seen = new Set();
  return attempts.filter((attempt) => {
    const key = `${attempt.url}|${attempt.query}`;
    if (seen.has(key)) return false;
    seen.add(key);
    return true;
  });
}

function buildSearchAttempt(url, query, options = {}) {
  return {
    url,
    query,
    kind: detectSearchTermKind(query),
    method: "GET",
    fallbackSourceType: options.fallbackSourceType || "",
  };
}

function buildHudySearchAttempts(base, product) {
  const attempts = [];
  if (product.ean) {
    attempts.push(buildSearchAttempt(`${base}/vyhledavani?q=${encodeURIComponent(product.ean)}`, product.ean));
  }

  const productName = expandProductNames(product.productName)[0];
  if (productName) {
    attempts.push(
      buildSearchAttempt(
        `https://www.google.com/search?q=${encodeURIComponent(`site:hudy.cz ${productName}`)}`,
        productName,
      ),
    );
  }

  return attempts;
}

function extractResponseUrlCandidate(url, html, product) {
  const title = extractHtmlTitle(html);
  if (!looksLikeProductCandidate(url, title, product)) return [];

  return [
    {
      title: title || url,
      url,
    },
  ];
}

function extractHtmlTitle(html) {
  return cleanHtmlText(String(html ?? "").match(/<title[^>]*>([\s\S]*?)<\/title>/i)?.[1] || "");
}

function findColorVariantUrl(html, pageUrl, color) {
  const wanted = normalizeColorText(color);
  if (!wanted) return "";

  const linkPattern = /<a\b([^>]*?)href=(?:"([^"]+)"|'([^']+)'|([^\s>]+))([^>]*)>([\s\S]*?)<\/a>/gi;
  for (const match of String(html ?? "").matchAll(linkPattern)) {
    const attributes = `${match[1]} ${match[5]}`;
    const label = cleanHtmlText(match[6]);
    const haystack = normalizeColorText(`${attributes} ${label}`);
    if (!haystack.includes(wanted)) continue;

    const href = match[2] || match[3] || match[4];
    const url = normalizeDirectResultUrl(href, pageUrl);
    if (url && isSameHost(url, getHostname(pageUrl))) return url;
  }

  return "";
}

async function verifyHudyVariant(url, product, browserEngine = "chromium") {
  try {
    const eanGroup = Array.isArray(product.eanGroup)
      ? product.eanGroup
      : String(product.eanGroup || product.ean || "").split(",");
    const wantedEans = new Set(
      eanGroup.map((value) => String(value || "").replace(/\D/g, "")).filter(Boolean),
    );
    const pages = [{ url, html: "" }];
    const visited = new Set();
    let matched = null;
    let lastStatus = 0;

    for (let index = 0; index < pages.length && index < 20; index += 1) {
      const page = pages[index];
      if (visited.has(page.url)) continue;
      visited.add(page.url);

      const response = await scrapePage(page.url, { engine: browserEngine, waitAfterLoadMs: 750 });
      const html = response.pageSource;
      lastStatus = response.status;
      const pageEans = extractPageEans(html);
      const matchedEan = pageEans.find((value) => wantedEans.has(value)) || "";
      const colorMatch = normalizeColorText(cleanHtmlText(html)).includes(normalizeColorText(product.color));

      if (matchedEan) {
        matched = {
          url: response.url || page.url,
          html,
          matchedEan,
          colorMatch,
          pageEans,
        };
        break;
      }

      for (const variantUrl of extractHudyVariantUrls(html, response.url || page.url)) {
        if (!visited.has(variantUrl) && !pages.some((candidate) => candidate.url === variantUrl)) {
          pages.push({ url: variantUrl, html: "" });
        }
      }
    }

    return {
      verified: Boolean(matched),
      matchedEan: matched?.matchedEan || "",
      pageEans: matched?.pageEans || [],
      colorMatch: matched?.colorMatch || false,
      matchedUrl: matched?.url || "",
      checkedVariants: visited.size,
      pictures: matched ? extractHudyGalleryPictureUrls(matched.html, matched.url) : [],
      httpStatus: lastStatus,
    };
  } catch (error) {
    return { verified: false, error: error.message || "Variant verification failed." };
  }
}

function extractPageEans(html) {
  return [...new Set(
    cleanHtmlText(html).match(/\b\d{8,14}\b/g) || [],
  )];
}

function extractHudyVariantUrls(html, pageUrl) {
  const urls = [];
  const linkPattern = /<a\b([^>]*?(?:product-change-color|change-color)[^>]*)>/gi;
  for (const match of String(html ?? "").matchAll(linkPattern)) {
    const href = match[1].match(/href=(?:"([^"]+)"|'([^']+)'|([^\s>]+))/i);
    const url = normalizeDirectResultUrl(href?.[1] || href?.[2] || href?.[3], pageUrl);
    if (url && isSameHost(url, getHostname(pageUrl)) && !urls.includes(url)) urls.push(url);
  }
  return urls;
}

function normalizeColorText(value) {
  return String(value ?? "")
    .toLowerCase()
    .replace(/&amp;/g, "&")
    .replace(/[_-]+/g, " ")
    .replace(/[^a-z0-9]+/g, " ")
    .replace(/\s+/g, " ")
    .trim();
}

function extractPictureUrls(html, pageUrl) {
  const source = String(html ?? "");
  const urls = [];
  const add = (value) => {
    const decoded = decodeHtmlEntities(String(value ?? "").trim());
    if (!decoded || decoded.startsWith("data:") || /\.svg(?:[?#]|$)/i.test(decoded)) return;
    try {
      const url = new URL(decoded, pageUrl).href;
      if (!/^https?:/i.test(url) || /(?:logo|icon|sprite|favicon)/i.test(url)) return;
      const largeUrl = url.replace(/\/w\d+h\d+-jpg\//i, "/w1000h1000-jpg/");
      if (largeUrl !== url && !urls.includes(largeUrl)) urls.push(largeUrl);
      if (!urls.includes(url)) urls.push(url);
    } catch {
      // Ignore malformed image references from the source page.
    }
  };

  for (const match of source.matchAll(/<meta\b[^>]*(?:property|name)=["'](?:og:image|twitter:image)["'][^>]*content=["']([^"']+)/gi)) {
    add(match[1]);
  }
  for (const match of source.matchAll(/(?:data-(?:original|large|full|zoom-image|image)|data-src)=["']([^"']+)/gi)) {
    add(match[1]);
  }
  for (const match of source.matchAll(/<a\b[^>]*href=["']([^"']+\.(?:jpe?g|png|webp)(?:\?[^"']*)?)["']/gi)) {
    add(match[1]);
  }
  for (const match of source.matchAll(/<img\b[^>]*(?:src|data-src)=["']([^"']+)/gi)) add(match[1]);
  for (const match of source.matchAll(/(?:"image"\s*:\s*|"imageUrl"\s*:\s*")["']?([^"'\\,} ]+)/gi)) add(match[1]);
  for (const match of source.matchAll(/srcset=["']([^"']+)/gi)) {
    for (const part of match[1].split(",")) add(part.trim().split(/\s+/)[0]);
  }

  return urls.slice(0, 12);
}

export function extractIdealoGalleryPictureUrls(html, pageUrl) {
  const source = String(html ?? "");
  const galleryMatches = [
    ...source.matchAll(/<li\b[^>]*class=["'][^"']*splide__slide[^"']*["'][^>]*>[\s\S]*?<\/li>/gi),
  ];
  const orderedGalleryUrls = [];

  for (const match of galleryMatches) {
    const best = pickBestImageUrlFromMarkup(match[0], pageUrl);
    if (best && !orderedGalleryUrls.includes(best)) orderedGalleryUrls.push(best);
  }

  if (orderedGalleryUrls.length) return orderedGalleryUrls.slice(0, 12);

  const trackMatch = source.match(/<div\b[^>]*class=["'][^"']*splide__track[^"']*["'][^>]*>[\s\S]*?<\/div>/i);
  if (trackMatch) {
    const best = pickBestImageUrlFromMarkup(trackMatch[0], pageUrl);
    if (best) return [best];
  }

  return [];
}

function pickBestImageUrlFromMarkup(markup, pageUrl) {
  const candidates = [];
  const add = (value, descriptorScore = 0) => {
    const normalized = normalizeImageCandidateUrl(value, pageUrl);
    if (!normalized) return;
    candidates.push({
      url: normalized,
      score: scoreImageCandidate(normalized, descriptorScore),
    });
  };

  for (const attribute of ["srcset", "data-srcset"]) {
    for (const match of markup.matchAll(new RegExp(`${attribute}=["']([^"']+)`, "gi"))) {
      for (const candidate of parseSrcsetCandidates(match[1], pageUrl)) {
        add(candidate.url, candidate.score);
      }
    }
  }

  for (const attribute of ["data-large", "data-original", "data-full", "data-zoom-image", "data-src", "src"]) {
    for (const match of markup.matchAll(new RegExp(`${attribute}=["']([^"']+)`, "gi"))) {
      add(match[1]);
    }
  }

  candidates.sort((a, b) => b.score - a.score);
  return candidates[0]?.url || "";
}

function parseSrcsetCandidates(value, pageUrl) {
  return String(value ?? "")
    .split(",")
    .map((part) => {
      const [urlPart, descriptor = ""] = part.trim().split(/\s+/);
      const descriptorScore = /(\d+)w/i.test(descriptor)
        ? Number(descriptor.match(/(\d+)w/i)?.[1] || 0)
        : /(\d+(?:\.\d+)?)x/i.test(descriptor)
          ? Number(descriptor.match(/(\d+(?:\.\d+)?)x/i)?.[1] || 0) * 400
          : 0;
      return {
        url: normalizeImageCandidateUrl(urlPart, pageUrl),
        score: descriptorScore,
      };
    })
    .filter((candidate) => candidate.url);
}

function normalizeImageCandidateUrl(value, pageUrl) {
  const decoded = decodeHtmlEntities(String(value ?? "").trim());
  if (!decoded || decoded.startsWith("data:") || /\.svg(?:[?#]|$)/i.test(decoded)) return "";

  try {
    const url = new URL(decoded.startsWith("//") ? `https:${decoded}` : decoded, pageUrl).href;
    if (!/^https?:/i.test(url)) return "";
    if (/(?:logo|icon|sprite|favicon|placeholder)/i.test(url)) return "";
    return url;
  } catch {
    return "";
  }
}

function scoreImageCandidate(url, descriptorScore = 0) {
  const dimensionScore = Math.max(
    ...[...String(url).matchAll(/(?:^|[^0-9])(\d{2,5})[x_-](\d{2,5})(?:[^0-9]|$)/g)].map(
      (match) => Number(match[1]) * Number(match[2]),
    ),
    ...[...String(url).matchAll(/w(\d{2,5})h(\d{2,5})/gi)].map(
      (match) => Number(match[1]) * Number(match[2]),
    ),
    0,
  );
  const largePathBonus = /(?:original|large|zoom|full|1000|1200|1500|2000)/i.test(url) ? 100000 : 0;
  const thumbnailPenalty = /(?:thumb|thumbnail|small|preview|\/s\d+_|_s\d+|150x150|80x80)/i.test(url)
    ? -100000
    : 0;

  return dimensionScore + descriptorScore + largePathBonus + thumbnailPenalty;
}

async function extractPicturesFromPage(url, browserEngine = "chromium") {
  try {
    const response = await scrapePage(url, { engine: browserEngine, waitAfterLoadMs: 750 });
    return extractHudyGalleryPictureUrls(response.pageSource, response.url || url);
  } catch {
    return [];
  }
}

function extractHudyGalleryPictureUrls(html, pageUrl) {
  const urls = [];
  const add = (value) => {
    const decoded = decodeHtmlEntities(String(value ?? "").trim());
    if (!decoded) return;
    const url = normalizeDirectResultUrl(decoded, pageUrl);
    if (!url || !/\/data\/images\/(?:pdbox1000w|w\d+h\d+)/i.test(url)) return;
    if (!urls.includes(url)) urls.push(url);
  };

  const tagPattern = /<a\b([^>]*?(?:js-product-top-img-link|js-product-top-img-target)[^>]*)>/gi;
  for (const match of String(html ?? "").matchAll(tagPattern)) {
    const href = match[1].match(/href=(?:"([^"]+)"|'([^']+)'|([^\s>]+))/i);
    add(href?.[1] || href?.[2] || href?.[3]);
  }

  return urls.filter((url) => /\/pdbox1000w\//i.test(url));
}

function detectSearchTermKind(query) {
  if (/^\d{8,14}$/.test(String(query))) return "EAN";
  if (/^[a-z0-9-]{3,}$/i.test(String(query)) && /\d/.test(String(query))) return "reference";
  return "name";
}

function buildSearchTerms(product) {
  const names = expandProductNames(product.productName);
  const terms = [];

  if (product.ean) terms.push(product.ean);
  if (product.supplierReference) terms.push(product.supplierReference);
  terms.push(...names);
  if (product.color) terms.push(`${names[0] || ""} ${product.color}`.trim());

  return dedupeStrings(terms);
}

function expandProductNames(name) {
  const text = String(name ?? "").trim();
  if (!text) return [];
  const names = [text];
  if (/\bWoman\b/i.test(text)) names.push(text.replace(/\bWoman\b/gi, "Women"));
  if (/\bWomen\b/i.test(text)) names.push(text.replace(/\bWomen\b/gi, "Woman"));
  return dedupeStrings(names);
}

function dedupeStrings(items) {
  return [...new Set(items.filter(Boolean))];
}

function extractDirectCandidates(html, source, product) {
  const sourceHost = getHostname(source.url);
  const candidates = [];
  const seen = new Set();
  const linkPattern = /<a\b[^>]*href=(?:"([^"]+)"|'([^']+)')[^>]*>([\s\S]*?)<\/a>/gi;

  for (const match of html.matchAll(linkPattern)) {
    const url = normalizeDirectResultUrl(match[1] || match[2], source.url);
    const title = cleanHtmlText(match[3]);
    if (!url || seen.has(url) || !isSameHost(url, sourceHost)) continue;
    if (!looksLikeProductCandidate(url, title, product)) continue;
    seen.add(url);
    candidates.push({
      title: title || url,
      url,
    });
  }

  return candidates;
}

function normalizeDirectResultUrl(value, baseUrl) {
  const text = decodeHtmlEntities(String(value ?? ""));
  try {
    return new URL(text, normalizeSourceUrl(baseUrl)).href;
  } catch {
    return "";
  }
}

function looksLikeProductCandidate(url, title, product) {
  const haystack = `${url} ${title}`.toLowerCase();
  const productWords = expandProductNames(product.productName)
    .flatMap((name) => name.toLowerCase().split(/\s+/))
    .map((word) => word.replace(/[^a-z0-9]+/g, ""))
    .filter((word) => word.length >= 4 && !isGenericProductWord(word));
  const matchedWords = productWords.filter((word) => haystack.includes(word));
  const hasSpecificProductWord = matchedWords.some((word) => word.length >= 6);
  const hasReference =
    product.supplierReference && haystack.includes(String(product.supplierReference).toLowerCase());
  const hasEan = product.ean && haystack.includes(String(product.ean).toLowerCase());
  const badPath =
    /\/(?:cart|checkout|login|account|customer|wishlist|compare)\b/i.test(url) ||
    /\/(?:vyhledavani|search|catalogsearch)\b/i.test(url) ||
    /[?&](?:search|query|q)=/i.test(url);

  return !badPath && (hasEan || hasReference || hasSpecificProductWord || matchedWords.length >= 2);
}

function isGenericProductWord(word) {
  return new Set(["trek", "gtx", "woman", "women", "mens", "womens", "boty", "shop"]).has(word);
}

function isSameHost(url, expectedHost) {
  if (!expectedHost) return true;
  const actualHost = getHostname(url);
  return actualHost === expectedHost || actualHost.endsWith(`.${expectedHost}`);
}

function getHostname(url) {
  try {
    return new URL(normalizeSourceUrl(url)).hostname.replace(/^www\./, "").toLowerCase();
  } catch {
    return "";
  }
}

function cleanHtmlText(value) {
  return decodeHtmlEntities(String(value ?? "").replace(/<[^>]*>/g, " "))
    .replace(/\s+/g, " ")
    .trim();
}

function decodeHtmlEntities(value) {
  return String(value ?? "")
    .replace(/&amp;/g, "&")
    .replace(/&quot;/g, "\"")
    .replace(/&#x27;/g, "'")
    .replace(/&#39;/g, "'")
    .replace(/&lt;/g, "<")
    .replace(/&gt;/g, ">");
}

async function checkMappingSourceUrls(items) {
  return Promise.all(
    items.map(async (item) => {
      if (!item.enabled || !item.url) {
        return {
          ...item,
          health: item.url ? "skipped" : "missing-url",
          httpStatus: "",
        };
      }

      return {
        ...item,
        ...(await checkUrl(item.url, item.browserEngine, item.browserHeadless)),
      };
    }),
  );
}

async function checkUrl(url, browserEngine = "chromium", headless = true) {
  try {
    const target = normalizeSourceUrl(url);
    const response = await scrapePage(target, {
      engine: browserEngine,
      headless,
      waitAfterLoadMs: 250,
    });

    return {
      health: response.ok ? "ok" : "warning",
      httpStatus: response.status,
    };
  } catch (error) {
    return {
      health: "error",
      httpStatus: "",
      error: error.message || "URL check failed.",
    };
  }
}

function normalizeSourceUrl(url) {
  const text = String(url ?? "").trim();
  if (/^https?:\/\//i.test(text)) return text;
  return `https://${text}`;
}

function normalizeProduct(row) {
  return {
    idProductCatalog: row.id_product_catalog,
    idManufacturer: row.id_manufacturer,
    codeManufacture: row.code_manufacture,
    vocDiscount: row.voc_discount,
    maxMocDiscount: row.max_moc_discount ?? row.max_moc_dicount,
    supplierReference: row.supplier_reference,
    catalogValidUntil: row.catalog_valid_until,
    name: row.name,
    priceMoc: row.price ?? row.price_moc ?? row.moc_price,
    combinations: row.combinations ?? row.combination,
    idAttributeGroup: row.id_attribute_group,
    idSupplier: row.id_supplier,
    idCategoryDefault: row.id_category_default,
    category9bName: row["9b_category_name"],
    manufacturerName: row.manufacturer_name,
    catalogCategory: row.catalog_category ?? row.category,
    gender: row.gender,
    position: row.position,
    total: row.total,
    photosLoaded: row.photos_loaded ?? getPhotosCount(row.photo_json),
    referenceSeparator: row.reference_separator,
    idProduct: row.id_product,
    price: row.price,
    supplierStock: row.skladovka,
    catalog9b: row.catalog_9b,
    imageUrl: row.image_url,
  };
}

function normalizeMappingSource(row, tableName) {
  return {
    id:
      row.mapping_id ??
      row.id_product_catalog_manufacturer_source ??
      row.id_product_catalog_source ??
      row.id ??
      "",
    key: row.source_key ?? row.source_code ?? row.code ?? row.type ?? "",
    name: row.source_name ?? row.name ?? row.title ?? row.source ?? "",
    type: row.source_type ?? row.type ?? row.kind ?? "",
    url: row.base_url ?? row.url ?? row.source_url ?? row.website ?? "",
    priority:
      row.mapping_priority ?? row.priority ?? row.source_priority ?? row.sort_order ?? row.position ?? "",
    searchPrimaryKey: row.search_primary_key ?? "",
    searchFallbackKeys: parseList(row.search_fallback_keys),
    fallbackSourceType: row.fallback_source_type ?? "",
    searchUrlTemplate: row.search_url_template ?? "",
    fallbackUrlTemplate: row.fallback_url_template ?? "",
    searchContextKeys: parseList(row.search_context_keys),
    accessMode: row.access_mode ?? "browser",
    browserEngine: String(row.browser_engine ?? "default").trim().toLowerCase(),
    browserHeadless: normalizeEnabled(row.browser_headless ?? 0),
    waitAfterLoadMs: Number(row.wait_after_load_ms) || 3000,
    scraperConfig: parseJsonValue(row.scraper_config, {}),
    capabilities: parseCapabilities(row.capabilities),
    enabled: normalizeEnabled(row.mapping_enabled ?? row.enabled ?? row.active ?? 1)
      && normalizeEnabled(row.enabled ?? 1),
    note: row.note ?? row.description ?? "",
    tableName,
  };
}

function parseCapabilities(value) {
  const parsed = parseJsonValue(value, []);
  if (!Array.isArray(parsed)) return [];

  return parsed
    .filter(Boolean)
    .filter((item) => item.capability)
    .map((item) => ({
      capability: item.capability,
      enabled: normalizeEnabled(item.enabled ?? 1),
      priority: Number(item.priority) || 100,
      parserKey: item.parserKey ?? "",
      parserModule: item.parserModule ?? "",
      browserEngine: String(item.browserEngine ?? "").trim().toLowerCase(),
      browserHeadless: item.browserHeadless === null ? null : normalizeEnabled(item.browserHeadless ?? 0),
      waitAfterLoadMs: item.waitAfterLoadMs === null ? null : Number(item.waitAfterLoadMs) || null,
      minWidth: item.minWidth === null ? null : Number(item.minWidth) || null,
      minHeight: item.minHeight === null ? null : Number(item.minHeight) || null,
      minLongestSide: item.minLongestSide === null ? null : Number(item.minLongestSide) || null,
      defaultTargetStrategy: item.defaultTargetStrategy ?? "unknown",
      targetStrategyOverride: item.targetStrategyOverride || "",
      selectorConfig: parseJsonValue(item.selectorConfig, {}),
      extractionConfig: parseJsonValue(item.extractionConfig, {}),
      qualityConfig: parseJsonValue(item.qualityConfig, {}),
      configOverride: parseJsonValue(item.configOverride, {}),
      note: item.note ?? "",
    }));
}

function parseJsonValue(value, fallback) {
  if (value === null || value === undefined || value === "") return fallback;
  if (typeof value === "object" && !Buffer.isBuffer(value)) return value;

  try {
    return JSON.parse(Buffer.isBuffer(value) ? value.toString("utf8") : String(value));
  } catch {
    return fallback;
  }
}

function parseList(value) {
  return String(value ?? "")
    .split(",")
    .map((item) => item.trim())
    .filter(Boolean);
}

function normalizeEnabled(value) {
  if (value === false) return false;
  if (value === true) return true;
  return String(value ?? "1") !== "0";
}

function isMissingMappingSchemaError(error) {
  return /doesn't exist|unknown column|1146|1054/i.test(error.message || "");
}

function normalizeCombination(row) {
  return {
    status: [row.catalog, row.status].filter(Boolean).join("/") || row.status,
    color: row.color,
    combi: normalizeCombinationValue(row.combi ?? row.combination ?? row.combinations),
    refer: row.refer ?? row.reference_extra,
    ean13: row.ean_13,
    nineB: row.catalog_9b,
    supplierStock: row.skladovka,
    hasProblem: false,
    id: row.id_product_catalog,
    voc: row.voc ?? row.wholesale_price,
    moc: row.moc ?? row.moc_price,
    productAttribute: row.product_attribute,
    raw: row,
  };
}

function normalizeProductInfo(row) {
  return {
    idProductCatalog: row.id_product_catalog,
    productAttribute: row.id_product_attribute,
    catalogColor: row.color_attribute_id,
    catalogCombi: row.combi_attribute_id,
    nineBEan13: row.nine_b_ean13,
    nineBVoc: row.nine_b_voc,
    nineBMoc: row.nine_b_moc,
    hasProblem: Number(row.has_problem) === 1,
    eanStatus: row.nine_b_ean13 && row.nine_b_ean13 !== row.catalog_ean13 ? "NEW EAN" : "",
    colorAttributeId: row.color_attribute_id,
    combiAttributeId: row.combi_attribute_id,
  };
}

function normalizeCombinationValue(value) {
  const text = String(value ?? "").trim();
  return /^\d+,\d+$/.test(text) ? text.replace(",", ".") : text;
}

function getPhotosCount(photoJson) {
  if (!photoJson) return "";

  try {
    const parsed = JSON.parse(photoJson);
    return Array.isArray(parsed.photos) ? `${parsed.photos.length} pict.` : "";
  } catch {
    return "";
  }
}
