/**
 * Lightweight job listing URL health check.
 *
 * Fail-open on network/timeout/5xx so flaky sites do not drop good jobs.
 * Hard-drop on 404/410 and common "expired / no longer available" page text.
 */

export type JobListingProbeResult = {
  expired: boolean;
  status: number | null;
  finalUrl: string;
};

const EXPIRED_URL_RE =
  /expired_jd_redirect|unavailable|no[_-]longer[_-]available|job[_-]expired|listing[_-]closed/i;

const EXPIRED_BODY_RE =
  /\bjob expired\b|\bthis job has expired\b|\bthis job listing is no longer available\b|\bno longer accepting applications\b|\bjob you were looking for is no longer available\b|\bthis job is no longer available\b|\bposition has been filled\b|\bjob posting is closed\b|\bthis posting has expired\b/i;

const GOOGLE_HOST_RE = /(?:^|\.)google\.[a-z.]+$/i;

export const DEFAULT_JOB_LISTING_PROBE_TIMEOUT_MS = 5_000;

export function isGoogleJobsHost(url: string): boolean {
  try {
    const host = new URL(url).hostname;
    return GOOGLE_HOST_RE.test(host);
  } catch {
    return false;
  }
}

/**
 * Prefer the real apply URL over Google's share permalink (share pages often
 * stay 200 after the underlying listing dies).
 */
export function resolveJobListingProbeUrl(args: {
  jobUrl?: string | null;
  applicationLink?: string | null;
}): string | null {
  const apply = args.applicationLink?.trim() || "";
  const jobUrl = args.jobUrl?.trim() || "";

  if (apply && !isGoogleJobsHost(apply)) return apply;
  if (jobUrl && !isGoogleJobsHost(jobUrl)) return jobUrl;
  if (apply) return apply;
  return jobUrl || null;
}

export function listingLooksExpired(args: {
  status: number;
  finalUrl: string;
  body: string;
}): boolean {
  if (args.status === 404 || args.status === 410 || args.status === 451) {
    return true;
  }
  if (EXPIRED_URL_RE.test(args.finalUrl)) return true;
  // Only scan a prefix — expiry banners are near the top.
  const sample = args.body.slice(0, 12_000);
  return EXPIRED_BODY_RE.test(sample);
}

async function fetchWithTimeout(
  url: string,
  timeoutMs: number,
  fetchImpl: typeof fetch,
): Promise<Response> {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);
  try {
    return await fetchImpl(url, {
      method: "GET",
      redirect: "follow",
      signal: controller.signal,
      headers: {
        Accept: "text/html,application/xhtml+xml",
        "User-Agent": "JobOps/1.0 (+job-listing-probe)",
      },
    });
  } finally {
    clearTimeout(timer);
  }
}

/**
 * Probe a listing URL. Returns `null` on transport failure (caller should keep).
 */
export async function probeJobListingUrl(
  url: string,
  options: {
    timeoutMs?: number;
    fetchImpl?: typeof fetch;
  } = {},
): Promise<JobListingProbeResult | null> {
  const trimmed = url?.trim();
  if (!trimmed) return null;

  const timeoutMs = options.timeoutMs ?? DEFAULT_JOB_LISTING_PROBE_TIMEOUT_MS;
  const fetchImpl = options.fetchImpl ?? fetch;

  try {
    const response = await fetchWithTimeout(trimmed, timeoutMs, fetchImpl);
    const finalUrl = response.url || trimmed;
    // 5xx: fail-open
    if (response.status >= 500) {
      return { expired: false, status: response.status, finalUrl };
    }
    const body = await response.text();
    const expired = listingLooksExpired({
      status: response.status,
      finalUrl,
      body,
    });
    return { expired, status: response.status, finalUrl };
  } catch {
    return null;
  }
}

export async function filterExpiredJobListings<
  T extends { jobUrl?: string | null; applicationLink?: string | null },
>(
  jobs: readonly T[],
  options: {
    concurrency?: number;
    timeoutMs?: number;
    fetchImpl?: typeof fetch;
    shouldStop?: () => boolean;
  } = {},
): Promise<{ kept: T[]; dropped: number }> {
  if (jobs.length === 0) return { kept: [], dropped: 0 };

  const concurrency = Math.max(
    1,
    Math.min(6, Math.floor(options.concurrency ?? 4)),
  );
  const keptFlags = Array.from({ length: jobs.length }, () => true);
  let nextIndex = 0;
  let dropped = 0;

  const worker = async () => {
    while (true) {
      if (options.shouldStop?.()) return;
      const index = nextIndex;
      nextIndex += 1;
      if (index >= jobs.length) return;

      const job = jobs[index];
      const probeUrl = resolveJobListingProbeUrl(job);
      if (!probeUrl) continue;

      const probe = await probeJobListingUrl(probeUrl, {
        timeoutMs: options.timeoutMs,
        fetchImpl: options.fetchImpl,
      });
      if (probe?.expired) {
        keptFlags[index] = false;
        dropped += 1;
      }
    }
  };

  await Promise.all(
    Array.from({ length: Math.min(concurrency, jobs.length) }, () => worker()),
  );

  return {
    kept: jobs.filter((_, index) => keptFlags[index]),
    dropped,
  };
}
