/**
 * Fetch a QAJobsBoard job detail page for richer JSON-LD and HTML than jobs.json alone.
 */

import { isPostingExpiredByDate } from "./application-link.js";

export interface QaJobDetailEnrichment {
  location?: string;
  jobDescription?: string;
  expired?: boolean;
}

const JSON_LD_RE =
  /<script[^>]*type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi;

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

export function stripHtml(html: string): string {
  const noTags = html.replace(/<[^>]+>/g, " ");
  return decodeHtmlEntities(noTags).replace(/\s+/g, " ").trim();
}

export function extractJobLocationFromText(text: string): string | undefined {
  const match = text.match(/\bjob\s+location\s*:\s*([^\n]+)/i);
  if (!match?.[1]) return undefined;
  let extracted = match[1].trim();
  extracted =
    extracted
      .split(
        /\s+(?=experience|shift timings|company profile|responsibilities|technical requirements)\b/i,
      )[0]
      ?.trim() ?? extracted;
  // Drop office footers like "India | UK | USA" glued onto the city line.
  extracted =
    extracted.split(/\s+(?=(?:india|uk|usa|us|canada)\s*\|)/i)[0]?.trim() ??
    extracted;
  if (extracted.length > 120) {
    extracted = extracted.slice(0, 120).trim();
  }
  return extracted || undefined;
}

function asRecord(value: unknown): Record<string, unknown> | null {
  return value && typeof value === "object" && !Array.isArray(value)
    ? (value as Record<string, unknown>)
    : null;
}

function parseJsonLdBlocks(html: string): unknown[] {
  const out: unknown[] = [];
  for (const match of html.matchAll(JSON_LD_RE)) {
    const raw = match[1]?.trim();
    if (!raw) continue;
    try {
      out.push(JSON.parse(raw));
    } catch {
      // ignore malformed blocks
    }
  }
  return out;
}

function findJobPostingNode(node: unknown): Record<string, unknown> | null {
  if (Array.isArray(node)) {
    for (const item of node) {
      const found = findJobPostingNode(item);
      if (found) return found;
    }
    return null;
  }
  const record = asRecord(node);
  if (!record) return null;
  const type = record["@type"];
  const types = Array.isArray(type) ? type : type ? [type] : [];
  if (types.some((t) => String(t).toLowerCase() === "jobposting")) {
    return record;
  }
  if (record["@graph"]) {
    return findJobPostingNode(record["@graph"]);
  }
  return null;
}

function formatPostalAddress(
  address: Record<string, unknown>,
): string | undefined {
  const parts = [
    address.addressLocality,
    address.addressRegion,
    address.addressCountry,
  ]
    .map((v) => (typeof v === "string" ? v.trim() : ""))
    .filter(Boolean);
  return parts.length > 0 ? parts.join(", ") : undefined;
}

function locationFromJobPosting(
  posting: Record<string, unknown>,
): string | undefined {
  const description =
    typeof posting.description === "string" ? posting.description : "";
  const fromDescription = extractJobLocationFromText(stripHtml(description));
  if (fromDescription) return fromDescription;

  const jobLocation = posting.jobLocation;
  if (Array.isArray(jobLocation)) {
    for (const entry of jobLocation) {
      const loc = locationFromJobLocationNode(entry);
      if (loc) return loc;
    }
  } else if (jobLocation) {
    const loc = locationFromJobLocationNode(jobLocation);
    if (loc) return loc;
  }

  return undefined;
}

function locationFromJobLocationNode(node: unknown): string | undefined {
  const record = asRecord(node);
  if (!record) return undefined;
  const address = asRecord(record.address);
  if (address) {
    return formatPostalAddress(address);
  }
  if (typeof record.name === "string" && record.name.trim()) {
    return record.name.trim();
  }
  return undefined;
}

function postingIsExpired(posting: Record<string, unknown>): boolean {
  const validThrough =
    typeof posting.validThrough === "string" ? posting.validThrough : undefined;
  return isPostingExpiredByDate(validThrough);
}

export function parseQaJobDetailPage(
  html: string,
): QaJobDetailEnrichment | null {
  for (const block of parseJsonLdBlocks(html)) {
    const posting = findJobPostingNode(block);
    if (!posting) continue;

    if (postingIsExpired(posting)) {
      return { expired: true };
    }

    const description =
      typeof posting.description === "string"
        ? stripHtml(posting.description)
        : undefined;
    const location = locationFromJobPosting(posting);

    if (!description && !location) continue;
    return {
      ...(location ? { location } : {}),
      ...(description ? { jobDescription: description } : {}),
    };
  }

  const bodyText = stripHtml(html);
  const location = extractJobLocationFromText(bodyText);
  if (!location && bodyText.length < 40) return null;
  return {
    ...(location ? { location } : {}),
    ...(bodyText ? { jobDescription: bodyText.slice(0, 12000) } : {}),
  };
}

export async function fetchQaJobDetailEnrichment(
  jobUrl: string,
  fetchImpl: typeof fetch = fetch,
): Promise<QaJobDetailEnrichment | null> {
  const response = await fetchImpl(jobUrl, {
    headers: {
      Accept: "text/html,application/xhtml+xml",
      "User-Agent": "JobOps/1.0",
    },
  });
  if (!response.ok) {
    throw new Error(`QAJobsBoard detail page ${response.status} for ${jobUrl}`);
  }
  const html = await response.text();
  return parseQaJobDetailPage(html);
}
