import { pool, markDetailsVerified, markImageVerified } from "./db";

const EXCLUDED_CATEGORIES = [
  "NonTransceiver",
  "Accessory",
  "Adapter / Converter",
  "Switch / Media Converter",
  "Switch / Network Infrastructure",
  "NIC / Adapter",
  "Mux / Passive Optical",
  "Product Family",
  "Loopback / Test Module",
];

const HEADERS = {
  "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
  Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
  "Accept-Language": "en-US,en;q=0.9",
};

type Candidate = {
  id: string;
  vendorName: string;
  partNumber: string;
  productUrl: string;
  formFactor: string | null;
  speedGbps: number | null;
  reachLabel: string | null;
  fiberType: string | null;
  imageVerified: boolean | null;
  detailsVerified: boolean | null;
};

type InferredDetails = {
  formFactor?: string;
  speed?: string;
  speedGbps?: number;
  reachLabel?: string;
  reachMeters?: number;
  fiberType?: string;
  wavelengths?: string;
};

function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function decodeEntities(value: string): string {
  return value
    .replace(/&amp;/g, "&")
    .replace(/&quot;/g, '"')
    .replace(/&#039;/g, "'")
    .replace(/&lt;/g, "<")
    .replace(/&gt;/g, ">");
}

function absolutizeUrl(src: string, baseUrl: string): string {
  try {
    return new URL(decodeEntities(src), baseUrl).toString();
  } catch {
    return "";
  }
}

function stripHtml(html: string): string {
  return html
    .replace(/<script[\s\S]*?<\/script>/gi, " ")
    .replace(/<style[\s\S]*?<\/style>/gi, " ")
    .replace(/<[^>]+>/g, " ")
    .replace(/\s+/g, " ")
    .trim();
}

function extractTitle(html: string): string {
  const og = html.match(/<meta[^>]+property=["']og:title["'][^>]+content=["']([^"']+)["'][^>]*>/i)
    || html.match(/<meta[^>]+content=["']([^"']+)["'][^>]+property=["']og:title["'][^>]*>/i);
  if (og) return decodeEntities(og[1]);
  const title = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
  return title ? decodeEntities(stripHtml(title[1])) : "";
}

function extractImageUrl(html: string, baseUrl: string): string | undefined {
  const meta = html.match(/<meta[^>]+property=["']og:image["'][^>]+content=["']([^"']+)["'][^>]*>/i)
    || html.match(/<meta[^>]+content=["']([^"']+)["'][^>]+property=["']og:image["'][^>]*>/i)
    || html.match(/<meta[^>]+name=["']twitter:image["'][^>]+content=["']([^"']+)["'][^>]*>/i)
    || html.match(/<meta[^>]+content=["']([^"']+)["'][^>]+name=["']twitter:image["'][^>]*>/i);
  if (meta) {
    const url = absolutizeUrl(meta[1], baseUrl);
    if (isLikelyProductImage(url)) return url;
  }

  const idImage = html.match(/<img[^>]+id=["']image["'][^>]+src=["']([^"']+)["'][^>]*>/i)
    || html.match(/<img[^>]+src=["']([^"']+)["'][^>]+id=["']image["'][^>]*>/i);
  if (idImage) {
    const url = absolutizeUrl(idImage[1], baseUrl);
    if (isLikelyProductImage(url)) return url;
  }

  const imgRegex = /<img\b[^>]*\bsrc=["']([^"']+)["'][^>]*>/gi;
  let match: RegExpExecArray | null;
  while ((match = imgRegex.exec(html)) !== null) {
    const tag = match[0];
    const url = absolutizeUrl(match[1], baseUrl);
    const alt = (tag.match(/\balt=["']([^"']*)["']/i)?.[1] || "").toLowerCase();
    const haystack = `${url} ${alt}`.toLowerCase();
    if (!isLikelyProductImage(url)) continue;
    if (/transceiver|optical|sfp|qsfp|osfp|xfp|module|base-/i.test(haystack)) return url;
  }

  return undefined;
}

function isLikelyProductImage(url: string): boolean {
  const lower = url.toLowerCase();
  if (!lower || !/^https?:\/\//.test(lower)) return false;
  if (!/\.(jpg|jpeg|png|webp)(\?|$)/.test(lower)) return false;
  if (/(logo|country|icon|sprite|banner|menu|payment|placeholder|no-image|company|featured-|solution|fofc|mdc|mmc)/.test(lower)) {
    return false;
  }
  return /(catalog\/product|\/uploads\/image\/|product|transceiver|sfp|qsfp|osfp|xfp)/.test(lower);
}

function inferDetails(text: string): InferredDetails {
  const lower = text.toLowerCase();
  const details: InferredDetails = {};

  const formFactors: Array<[RegExp, string]> = [
    [/\bqsfp-dd\b/i, "QSFP-DD"],
    [/\bqsfp112\b/i, "QSFP112"],
    [/\bosfp112\b/i, "OSFP112"],
    [/\bosfp\b/i, "OSFP"],
    [/\bqsfp56\b/i, "QSFP56"],
    [/\bqsfp28\b/i, "QSFP28"],
    [/\bqsfp\+|\bqsfp\b/i, "QSFP+"],
    [/\bsfp56\b/i, "SFP56"],
    [/\bsfp28\b/i, "SFP28"],
    [/\bsfp\+|\bsfp plus\b/i, "SFP+"],
    [/\bxfp\b/i, "XFP"],
    [/\bsfp\b/i, "SFP"],
  ];
  for (const [regex, formFactor] of formFactors) {
    if (regex.test(text)) {
      details.formFactor = formFactor;
      break;
    }
  }

  const speedPatterns: Array<[RegExp, string, number]> = [
    [/\b800\s*g(?:base)?\b/i, "800G", 800],
    [/\b400\s*g(?:base)?\b/i, "400G", 400],
    [/\b200\s*g(?:base)?\b/i, "200G", 200],
    [/\b128\s*gfc\b/i, "128G FC", 128],
    [/\b100\s*g(?:base)?\b/i, "100G", 100],
    [/\b50\s*g(?:base)?\b/i, "50G", 50],
    [/\b40\s*g(?:base)?\b/i, "40G", 40],
    [/\b32\s*gfc\b/i, "32G FC", 32],
    [/\b16\s*gfc\b/i, "16G FC", 16],
    [/\b10\s*g(?:base)?\b/i, "10G", 10],
    [/\b1[\s.-]?25\s*g(?:b(?:ase)?|bps)?\b|\b1000base\b|\b1\s*g(?:base)?\b/i, "1G", 1],
    [/(^|[^.\d-])25\s*g(?:base)?\b/i, "25G", 25],
    [/\b2[\s.-]?5\s*g(?:base)?\b/i, "2.5G", 2.5],
  ];
  for (const [regex, speed, speedGbps] of speedPatterns) {
    if (regex.test(text)) {
      details.speed = speed;
      details.speedGbps = speedGbps;
      break;
    }
  }

  const kmMatch = text.match(/\b(160|120|100|80|60|40|30|20|10|3|2|1(?:\.4)?)\s*km\b/i);
  const mMatch = text.match(/\b(550|500|400|300|150|100|80|30)\s*m(?:eters?)?\b/i);
  if (kmMatch) {
    const km = parseFloat(kmMatch[1]);
    details.reachLabel = `${kmMatch[1]}km`;
    details.reachMeters = Math.round(km * 1000);
  } else if (mMatch) {
    const meters = parseInt(mMatch[1], 10);
    details.reachLabel = `${meters}m`;
    details.reachMeters = meters;
  } else if (/\bfr4\b/i.test(text)) {
    details.reachLabel = "2km";
    details.reachMeters = 2000;
  } else if (/\bdr\d*\b/i.test(text)) {
    details.reachLabel = "500m";
    details.reachMeters = 500;
  } else if (/\blr4?\b/i.test(text)) {
    details.reachLabel = "10km";
    details.reachMeters = 10000;
  } else if (/\ber4?\b/i.test(text)) {
    details.reachLabel = "40km";
    details.reachMeters = 40000;
  } else if (/\bzr4?\b/i.test(text)) {
    details.reachLabel = "80km";
    details.reachMeters = 80000;
  } else if (/\bsr\d*|\bsw4\b/i.test(text)) {
    details.reachLabel = "300m";
    details.reachMeters = 300;
  }

  if (/base-?t|rj-?45|cat ?6|cat ?5|copper/i.test(text)) {
    details.fiberType = "Copper";
  } else if (/single.?mode|\bsmf\b|\blr\b|\ber\b|\bzr\b|\bfr\b|\bdr\b|cwdm|dwdm|bidi|psm/i.test(lower)) {
    details.fiberType = "SMF";
  } else if (/multi.?mode|\bmmf\b|\bsr\b|\bsw\b/i.test(lower)) {
    details.fiberType = "MMF";
  }

  const wavelength = text.match(/\b(\d{3,4})\s*nm\b/i);
  if (wavelength) details.wavelengths = wavelength[1];

  return details;
}

function mergeDetails(existing: Candidate, inferred: InferredDetails): InferredDetails {
  const updates: InferredDetails = {};
  if (inferred.formFactor && (!existing.formFactor || existing.formFactor === "SFP+" && inferred.formFactor.startsWith("QSFP"))) {
    updates.formFactor = inferred.formFactor;
  }
  if (inferred.speedGbps && (!existing.speedGbps || inferred.speedGbps > existing.speedGbps)) {
    updates.speed = inferred.speed;
    updates.speedGbps = inferred.speedGbps;
  }
  if (inferred.reachLabel && !existing.reachLabel) {
    updates.reachLabel = inferred.reachLabel;
    updates.reachMeters = inferred.reachMeters;
  }
  if (inferred.fiberType && !existing.fiberType) {
    updates.fiberType = inferred.fiberType;
  }
  return updates;
}

async function fetchHtml(url: string): Promise<string> {
  const resp = await fetch(url, { headers: HEADERS, signal: AbortSignal.timeout(30000) });
  if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
  return resp.text();
}

async function applyDetailUpdates(id: string, updates: InferredDetails): Promise<void> {
  const sets: string[] = [];
  const values: unknown[] = [id];
  let idx = 2;

  if (updates.formFactor) {
    sets.push(`form_factor = $${idx++}`);
    values.push(updates.formFactor);
  }
  if (updates.speedGbps) {
    sets.push(`speed_gbps = $${idx++}`);
    values.push(updates.speedGbps);
  }
  if (updates.speed) {
    sets.push(`speed = $${idx++}`);
    values.push(updates.speed);
  }
  if (updates.reachMeters) {
    sets.push(`reach_meters = $${idx++}`);
    values.push(updates.reachMeters);
  }
  if (updates.reachLabel) {
    sets.push(`reach_label = $${idx++}`);
    values.push(updates.reachLabel);
  }
  if (updates.fiberType) {
    sets.push(`fiber_type = $${idx++}`);
    values.push(updates.fiberType);
  }
  if (updates.wavelengths) {
    sets.push(`wavelengths = COALESCE(NULLIF(wavelengths, ''), $${idx++})`);
    values.push(updates.wavelengths);
  }

  if (sets.length === 0) return;
  sets.push("updated_at = NOW()");
  await pool.query(`UPDATE transceivers SET ${sets.join(", ")} WHERE id = $1`, values);
}

async function main(): Promise<void> {
  const vendorFilter = (process.env["PRODUCT_ASSET_VENDOR"] || "").trim();
  const limit = parseInt(process.env["PRODUCT_ASSET_LIMIT"] || "120", 10);
  const apply = process.env["PRODUCT_ASSET_APPLY"] === "1";

  const vendorNames = vendorFilter
    ? vendorFilter.split(",").map((v) => v.trim()).filter(Boolean)
    : ["T&S Communication", "10Gtek", "GBICS"];

  const candidates = await pool.query<Candidate>(
    `SELECT t.id,
            v.name AS "vendorName",
            t.part_number AS "partNumber",
            t.product_page_url AS "productUrl",
            t.form_factor AS "formFactor",
            t.speed_gbps AS "speedGbps",
            t.reach_label AS "reachLabel",
            t.fiber_type AS "fiberType",
            t.image_verified AS "imageVerified",
            t.details_verified AS "detailsVerified"
     FROM transceivers t
     JOIN vendors v ON v.id = t.vendor_id
     WHERE v.name = ANY($1)
       AND COALESCE(t.category, '') <> ALL($2)
       AND COALESCE(t.product_page_url, '') ~* '^https?://'
       AND (
         COALESCE(t.image_verified, false) = false
         OR COALESCE(t.details_verified, false) = false
         OR t.speed_gbps IS NULL
         OR t.speed_gbps = 0
         OR COALESCE(t.fiber_type, '') = ''
       )
     ORDER BY v.name, t.part_number
     LIMIT $3`,
    [vendorNames, EXCLUDED_CATEGORIES, limit],
  );

  let images = 0;
  let details = 0;
  let skipped = 0;
  let errors = 0;

  console.log("=== Product page asset verifier ===", { vendorNames, limit, apply, count: candidates.rows.length });

  for (const row of candidates.rows) {
    try {
      const html = await fetchHtml(row.productUrl);
      const title = extractTitle(html);
      const text = `${row.partNumber} ${row.productUrl} ${title}`;
      const imageUrl = extractImageUrl(html, row.productUrl);
      const inferred = inferDetails(text);
      const updates = mergeDetails(row, inferred);

      if (!apply) {
        console.log("dry-run product asset", {
          vendor: row.vendorName,
          partNumber: row.partNumber,
          imageUrl,
          updates,
        });
      } else {
        await applyDetailUpdates(row.id, updates);
        if (imageUrl && !row.imageVerified) {
          const changed = await markImageVerified(row.id, imageUrl);
          if (changed) images++;
        }
        const marked = await markDetailsVerified({ transceiverId: row.id, sourceUrl: row.productUrl });
        if (marked) details++;
      }

      if (!imageUrl && Object.keys(updates).length === 0) skipped++;
      await sleep(750);
    } catch (err) {
      errors++;
      console.warn(`asset verifier error for ${row.vendorName} ${row.partNumber}: ${(err as Error).message}`);
    }
  }

  console.log("Product page asset verifier complete", { images, details, skipped, errors, apply });
}

if (require.main === module) {
  main()
    .then(() => pool.end())
    .catch((err) => {
      console.error("Fatal:", err);
      pool.end();
      process.exit(1);
    });
}
