import * as cheerio from "cheerio";
import { HttpStatusError } from "./httpErrors.js";
import { canonicalize } from "./types.js";
import type {
  AdapterOfferRefreshResult,
  FetchContext,
  Offer,
  OfferRefreshTarget,
  StoreAdapter,
} from "./types.js";
import { inStockFromText, validPrice } from "../adapters/util.js";
import { parsePriceEur } from "./normalize.js";

type Json = Record<string, unknown>;

function records(value: unknown): Json[] {
  if (Array.isArray(value)) return value.flatMap(records);
  if (!value || typeof value !== "object") return [];
  const record = value as Json;
  return [record, ...records(record["@graph"]), ...records(record["itemListElement"])];
}

function text(value: unknown): string {
  if (typeof value === "string") return value.trim();
  if (value && typeof value === "object") {
    const record = value as Json;
    return text(record["name"]) || text(record["value"]);
  }
  return "";
}

function productJsonLd(html: string): Json | null {
  const $ = cheerio.load(html);
  for (const element of $('script[type="application/ld+json"]').toArray()) {
    try {
      for (const record of records(JSON.parse($(element).text()))) {
        const kind = record["@type"];
        if (kind === "Product" || (Array.isArray(kind) && kind.includes("Product"))) return record;
      }
    } catch {
      // Ignore unrelated invalid structured-data blocks.
    }
  }
  return null;
}

function offerData(product: Json): Json | null {
  const value = product["offers"];
  if (Array.isArray(value)) return (value.find((item) => item && typeof item === "object") as Json | undefined) ?? null;
  return value && typeof value === "object" ? value as Json : null;
}

function jsonLdOffer(adapter: StoreAdapter, target: OfferRefreshTarget, html: string): Offer | null {
  const product = productJsonLd(html);
  if (!product) return null;
  const sale = offerData(product);
  const priceEur = Number(sale?.["price"] ?? sale?.["lowPrice"]);
  const identities = [text(product["sku"]), text(product["mpn"])].filter(Boolean);
  const partNumber = identities.find((value) => canonicalize(value) === canonicalize(target.expectedPartNumber)) ?? identities[0] ?? "";
  if (!validPrice(priceEur) || !partNumber) return null;
  const availabilitySchema = text(sale?.["availability"]);
  const inStock = schemaStock(availabilitySchema);
  return {
    store: adapter.id,
    brand: text(product["brand"]) || text(product["manufacturer"]) || null,
    partNumber,
    title: text(product["name"]) || partNumber,
    priceEur,
    inStock,
    availability: availabilitySchema ? (inStock ? "Pieejams" : /OutOfStock$/i.test(availabilitySchema) ? "Nav pieejams" : availabilitySchema) : null,
    deliveryNote: null,
    url: text(product["url"]) || target.url,
    imageUrl: text(product["image"]) || null,
    attributes: [],
    sourceRef: target.sourceRef,
  };
}

function schemaStock(availability: string): boolean | null {
  if (!availability) return null;
  if (/(?:InStock|LimitedAvailability|OnlineOnly)$/i.test(availability)) return true;
  if (/(?:OutOfStock|SoldOut|Discontinued|BackOrder|PreOrder)$/i.test(availability)) return false;
  return null;
}

function scopedMarkupOffer(adapter: StoreAdapter, target: OfferRefreshTarget, html: string): Offer | null {
  const $ = cheerio.load(html);
  const pageText = $.root().text().replace(/\s+/g, " ");
  const expectedPart = canonicalize(target.expectedPartNumber);
  if (!expectedPart || !canonicalize(pageText).includes(expectedPart)) return null;
  if (target.expectedBrand && !canonicalize(pageText).includes(canonicalize(target.expectedBrand))) return null;

  let priceEur: number;
  let availability: string | null = null;
  let deliveryNote: string | null = null;
  let inStockOverride: boolean | null | undefined;
  if (adapter.id === "handler") {
    let best: { price: number; availability: string; deliveryNote: string | null; quantity: number | null } | null = null;
    for (const element of $(".selected-part-offer-table-row").toArray()) {
      const row = $(element);
      const price = parsePriceEur(row.find(".font-semibold.whitespace-nowrap").first().text());
      const availabilityCell = row.find("td, .selected-part-offer-table-row-td").first();
      const status = availabilityCell.find(".text-gray-600, .text-red-300").first().text().replace(/\s+/g, " ").trim();
      const quantityMatch = status.match(/(\d+)\s*g(?:b|ab)\.?/i);
      const quantity = quantityMatch?.[1] ? Number(quantityMatch[1]) : null;
      const availabilityText = quantity !== null
        ? `Pieejams (${quantity} gab.)`
        : /uz pasūtījumu/i.test(status)
          ? "Uz pasūtījumu"
          : status;
      const date = availabilityCell.find(".text-gray-500").last().text().replace(/\s+/g, " ").trim();
      const candidate = { price, availability: availabilityText, deliveryNote: date ? `Nosūtīšana: ${date}` : null, quantity };
      if (validPrice(price) && (!best || handlerRowIsBetter(candidate, best))) best = candidate;
    }
    if (best) {
      priceEur = best.price;
      availability = best.availability || null;
      deliveryNote = best.deliveryNote;
      inStockOverride = best.quantity === null ? null : best.quantity > 0;
    } else {
      priceEur = NaN;
    }
  } else if (adapter.id === "trodo") {
    const scope = $(".product-main-info").first().parent();
    priceEur = parsePriceEur(scope.find(".price-block .price").first().text());
    if (!validPrice(priceEur)) {
      const description = $('meta[name="description"], meta[property="og:description"]').first().attr("content") ?? "";
      const match = description.match(/(?:Tikai|Only)\s+(\d+[.,]\d{2})\s*EUR/i);
      priceEur = match?.[1] ? parsePriceEur(match[1]) : NaN;
    }
    availability = scope.find(".on-stock, .out-of-stock, [class*=stock-status]").first().text().trim() || null;
    inStockOverride = availability ? inStockFromText(availability) : null;
  } else if (adapter.id === "xparts") {
    const exact = $(`[data-ga-id]`).filter((_, el) => canonicalize($(el).attr("data-ga-id") ?? "") === expectedPart).first();
    const scope = exact.closest("[data-product-id], .product, .product-info, main");
    priceEur = Number(exact.attr("data-ga-price") ?? scope.find("[data-ga-price]").first().attr("data-ga-price"));
    availability = scope.find(".availability, .stock, [class*=stock]").first().text().trim() || null;
  } else if (adapter.id === "ic24") {
    priceEur = Number($("#price_gross_2, .price_gross_2").first().attr("data-price"));
  } else {
    const main = $("main, [itemtype*=Product], .product-info, .product-page, body").first();
    priceEur = Number(main.find('[itemprop="price"][content]').first().attr("content") ?? main.find("[data-price]").first().attr("data-price"));
    const availabilityNode = main.find('[itemprop="availability"], .availability, .i_del-text, [class*=stock]').first();
    const availabilityRaw = availabilityNode.attr("content") ?? availabilityNode.text().trim();
    if (/InStock$/i.test(availabilityRaw)) {
      availability = "Pieejams";
      inStockOverride = true;
    } else if (/OutOfStock$/i.test(availabilityRaw)) {
      availability = "Nav pieejams";
      inStockOverride = false;
    } else {
      availability = availabilityRaw || null;
    }
  }
  if (!validPrice(priceEur)) return null;
  if ((adapter.id === "trodo" || adapter.id === "euautodalas") && !availability) return null;
  return {
    store: adapter.id,
    brand: target.expectedBrand,
    partNumber: target.expectedPartNumber,
    title: $("h1").first().text().replace(/\s+/g, " ").trim() || target.expectedPartNumber,
    priceEur,
    inStock: inStockOverride !== undefined ? inStockOverride : (availability ? inStockFromText(availability) : null),
    availability,
    deliveryNote,
    url: target.url,
    imageUrl: null,
    attributes: [],
    sourceRef: target.sourceRef,
  };
}

function handlerRowIsBetter(
  candidate: { price: number; quantity: number | null },
  current: { price: number; quantity: number | null },
): boolean {
  if (candidate.price !== current.price) return candidate.price < current.price;
  // Equal-price supplier rows can reorder. Prefer explicit stock, then the larger
  // quantity, so refresh output remains stable across page renders.
  return (candidate.quantity ?? -1) > (current.quantity ?? -1);
}

export async function refreshExactProduct(
  adapter: StoreAdapter,
  target: OfferRefreshTarget,
  ctx: FetchContext,
): Promise<AdapterOfferRefreshResult> {
  let html: string;
  try {
    if ((adapter.fetchMode ?? "auto") === "auto" && adapter.id !== "handler") {
      html = await ctx.http.get(adapter.id, target.url, { timeoutMs: adapter.timeoutMs });
    } else if (adapter.id === "ic24") {
      html = await ctx.browser.fetchOriginHtml(adapter.id, adapter.storeHomepage, target.url);
    } else {
      html = await ctx.browser.fetchPageHtml(
        adapter.id,
        target.url,
        adapter.id === "trodo" ? { waitForSelector: ".on-stock, .out-of-stock" } : {},
      );
    }
  } catch (error) {
    if (error instanceof HttpStatusError && (error.status === 404 || error.status === 410)) {
      return { status: "not_found" };
    }
    throw error;
  }
  const offer = jsonLdOffer(adapter, target, html) ?? scopedMarkupOffer(adapter, target, html);
  if (!offer) throw new DirectRefreshParseError(adapter.id);
  return { status: "found", offer };
}

export class DirectRefreshParseError extends Error {
  constructor(storeId: string) {
    super(`${storeId}: exact product page could not be parsed`);
    this.name = "DirectRefreshParseError";
  }
}
