#!/usr/bin/env node

import { pathToFileURL } from "node:url";

import {
  clickPhotoEntryPoint,
  createPageSession,
  dismissCommonOverlays,
  fail,
  gotoListing,
  isPageClosedError,
  normalizeImageCandidates,
  parseTarget,
  runWithOperationTimeout,
  scrollUntilSettled,
  sleep,
  waitForPhotoExperience,
} from "./real-estate-photo-common.js";
import {
  extractZillowStructuredPhotoCandidatesFromNextDataScript,
  shouldUseStructuredZillowPhotos,
} from "./zillow-photo-data.js";

const ZILLOW_LABELS = [
  /^See all(?: \d+)? photos$/i,
  /^See all photos$/i,
  /^Photos$/i,
];

async function getAnnouncedPhotoCount(page) {
  return page.evaluate(() => {
    const text = document.body?.innerText || "";
    const match = text.match(/See all\s+(\d+)\s+photos/i);
    return match ? Number(match[1]) : null;
  });
}

async function getMetaDescriptionPhotoCount(page) {
  return page.evaluate(() => {
    const content =
      document.querySelector('meta[name="description"]')?.getAttribute("content") ||
      document.querySelector('meta[property="og:description"]')?.getAttribute("content") ||
      "";
    const match = content.match(/\b(\d+)\s+photos\b/i);
    return match ? Number(match[1]) : null;
  });
}

function collapseZillowPhotos(candidates) {
  const byBaseId = new Map();

  for (const candidate of candidates) {
    const filename = candidate.pathname.split("/").pop() || "";
    const baseId = filename.split("-")[0];
    const sizeScore = (candidate.width || 0) * (candidate.height || 0) || candidate.width || candidate.height || 0;
    const preference = /-p_d\.(?:jpe?g|webp)$/i.test(candidate.url)
      ? 10_000_000
      : Number(candidate.url.match(/-cc_ft_(\d+)\./i)?.[1] || 0);
    const score = preference + sizeScore;
    const existing = byBaseId.get(baseId);
    const existingSizeScore = existing
      ? (existing.width || 0) * (existing.height || 0) || existing.width || existing.height || 0
      : -1;
    const existingPreference = existing
      ? /-p_d\.(?:jpe?g|webp)$/i.test(existing.url)
        ? 10_000_000
        : Number(existing.url.match(/-cc_ft_(\d+)\./i)?.[1] || 0)
      : 0;
    const existingScore = existing ? existingPreference + existingSizeScore : -1;

    if (!existing || score > existingScore) {
      byBaseId.set(baseId, candidate);
    }
  }

  return Array.from(byBaseId.values()).sort((a, b) => a.url.localeCompare(b.url));
}

async function collectZillowPhotoCandidates(page) {
  return page.evaluate(() => {
    const out = [];
    const add = (url, width, height) => {
      if (url) out.push({ url, width: Number(width || 0), height: Number(height || 0) });
    };
    const parseSrcset = (srcset) =>
      (srcset || "")
        .split(",")
        .map((entry) => entry.trim().split(/\s+/)[0])
        .filter(Boolean);

    const selectors = [
      ".media-stream-tile img",
      ".media-stream-tile source",
      '[class*="media-stream"] img',
      '[class*="media-stream"] source',
      'img[alt*="image of "]',
      'img[alt*="image of this home"]',
    ];

    const nodes = selectors.flatMap((selector) => Array.from(document.querySelectorAll(selector)));
    for (const node of nodes) {
      if (node instanceof HTMLImageElement) {
        add(node.currentSrc || node.src, node.naturalWidth || node.clientWidth, node.naturalHeight || node.clientHeight);
        for (const url of parseSrcset(node.srcset)) {
          add(url, node.naturalWidth || node.clientWidth, node.naturalHeight || node.clientHeight);
        }
      } else if (node instanceof HTMLSourceElement) {
        for (const url of parseSrcset(node.srcset)) {
          add(url, 0, 0);
        }
      }
    }

    return out;
  });
}

async function collectZillowStructuredPhotoCandidates(page) {
  const scriptText = await page.locator("#__NEXT_DATA__").textContent().catch(() => null);
  return extractZillowStructuredPhotoCandidatesFromNextDataScript(scriptText || "");
}

function normalizeZillowPhotos(candidates) {
  const normalized = normalizeImageCandidates(candidates, {
    hostIncludes: ["photos.zillowstatic.com"],
    minWidth: 240,
    minHeight: 180,
  });
  return collapseZillowPhotos(normalized);
}

export async function extractZillowPhotos(rawUrl, options = {}) {
  const requestedUrl = parseTarget(rawUrl);
  const maxAttempts = 2;
  let lastError = null;

  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
    const { context, page } = await createPageSession({ headless: process.env.HEADLESS !== "false" });
    const closeContext = async () => {
      await context.close().catch(() => {});
    };

    try {
      return await runWithOperationTimeout(
        "Zillow photo extraction",
        async () => {
          await gotoListing(page, requestedUrl);
          await dismissCommonOverlays(page);

          const expectedPhotoCount = await getAnnouncedPhotoCount(page);
          const fallbackPhotoCount = await getMetaDescriptionPhotoCount(page);
          const initialStructuredPhotos = normalizeZillowPhotos(
            await collectZillowStructuredPhotoCandidates(page)
          );

          if (
            shouldUseStructuredZillowPhotos(initialStructuredPhotos, {
              expectedPhotoCount,
              fallbackPhotoCount,
            })
          ) {
            const notes = [
              "The rendered Zillow listing shell already exposed a complete structured photo set, so extraction completed without relying on the all-photos click path.",
            ];
            if (!expectedPhotoCount && fallbackPhotoCount) {
              notes.push(
                `Trusted the embedded Zillow photo set using the page metadata count of ${fallbackPhotoCount} photos.`
              );
            } else if (!expectedPhotoCount && !fallbackPhotoCount) {
              notes.push(
                "Trusted a substantial embedded Zillow photo set even though the page did not expose an explicit photo count."
              );
            }
            if (attempt > 1) {
              notes.push(
                "Recovered after retrying Zillow photo extraction once because the first browser session closed unexpectedly."
              );
            }
            return {
              source: "zillow",
              requestedUrl,
              finalUrl: page.url(),
              title: await page.title(),
              clickedLabel: null,
              expectedPhotoCount: expectedPhotoCount || fallbackPhotoCount || null,
              complete: true,
              photoCount: initialStructuredPhotos.length,
              imageUrls: initialStructuredPhotos.map((photo) => photo.url),
              notes,
            };
          }

          const beforeUrl = page.url();
          let clickedLabel = null;
          let clickError = null;

          try {
            clickedLabel = await clickPhotoEntryPoint(page, ZILLOW_LABELS);
            await waitForPhotoExperience(page, beforeUrl);
            await scrollUntilSettled(page);
            await sleep(1200);
          } catch (error) {
            clickError = error instanceof Error ? error.message : String(error);
          }

          const [structuredCandidates, renderedCandidates] = await Promise.all([
            collectZillowStructuredPhotoCandidates(page),
            collectZillowPhotoCandidates(page),
          ]);
          const photos = normalizeZillowPhotos([...structuredCandidates, ...renderedCandidates]);

          if (!photos.length) {
            fail(
              "Zillow photo extraction failed.",
              clickError || "No Zillow image URLs were found on the rendered listing page."
            );
          }

          const complete = expectedPhotoCount ? photos.length >= expectedPhotoCount : true;
          const notes = [];
          if (clickedLabel) {
            notes.push("Opened Zillow all-photos flow and extracted direct Zillow image URLs.");
          } else {
            notes.push("The rendered Zillow listing shell already exposed the Zillow photo stream, so extraction completed without relying on the all-photos click path.");
          }
          if (clickError) {
            notes.push(`All-photos click path was not required: ${clickError}`);
          }
          if (attempt > 1) {
            notes.push(`Recovered after retrying Zillow photo extraction once because the first browser session closed unexpectedly.`);
          }

          return {
            source: "zillow",
            requestedUrl,
            finalUrl: page.url(),
            title: await page.title(),
            clickedLabel,
            expectedPhotoCount,
            complete,
            photoCount: photos.length,
            imageUrls: photos.map((photo) => photo.url),
            notes,
          };
        },
        {
          timeoutMs: Number(options.timeoutMs || 0) || undefined,
          onTimeout: closeContext
        }
      );
    } catch (error) {
      lastError = error;
      if (!(attempt < maxAttempts && isPageClosedError(error))) {
        throw new Error(error instanceof Error ? error.message : String(error));
      }
    } finally {
      await closeContext();
    }
  }

  throw new Error(lastError instanceof Error ? lastError.message : String(lastError));
}

async function main() {
  try {
    const result = await extractZillowPhotos(process.argv[2]);
    process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
  } catch (error) {
    fail("Zillow photo extraction failed.", error instanceof Error ? error.message : String(error));
  }
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
  main();
}
