/**
 * Built In — tech job board (JSON-LD ItemList embedded in SSR HTML).
 *
 * https://builtin.com/jobs
 */

import type {
  ExtractorManifest,
  ExtractorRunResult,
} from "@shared/types/extractors";
import type { CreateJobInput } from "@shared/types/jobs";

const ORIGIN = "https://builtin.com";

interface BuiltinListItem {
  title: string;
  url: string;
  description?: string;
}

function asString(value: unknown): string | undefined {
  if (typeof value !== "string") return undefined;
  const trimmed = value.trim();
  return trimmed ? trimmed : undefined;
}

function decodeJsonString(value: string): string {
  return value
    .replace(/\\"/g, '"')
    .replace(/\\n/g, "\n")
    .replace(/\\t/g, "\t")
    .replace(/\\\\/g, "\\");
}

function readMaxPages(raw: string | undefined): number {
  const parsed = raw ? Number.parseInt(raw, 10) : Number.NaN;
  if (!Number.isFinite(parsed)) return 3;
  return Math.min(Math.max(parsed, 1), 15);
}

function parseListItems(html: string): BuiltinListItem[] {
  const items: BuiltinListItem[] = [];
  const pattern =
    /\{"@type":"ListItem","position":\d+,"name":"((?:\\.|[^"\\])*)","url":"(https:\/\/builtin\.com\/job\/[^"]+)"(?:,"description":"((?:\\.|[^"\\])*)")?\}/g;

  for (const match of html.matchAll(pattern)) {
    const title = decodeJsonString(match[1] ?? "");
    const url = match[2];
    if (!title || !url) continue;
    items.push({
      title,
      url,
      description: match[3] ? decodeJsonString(match[3]) : undefined,
    });
  }

  return items;
}

function slugToWords(slug: string): string {
  return slug
    .split("-")
    .filter(Boolean)
    .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
    .join(" ");
}

function employerFromUrl(jobUrl: string): string {
  const match = jobUrl.match(/\/job\/[^/]+\/(\d+)/);
  if (!match) return "Unknown Employer";
  const slugMatch = jobUrl.match(/\/job\/([^/]+)\/\d+/);
  const slug = slugMatch?.[1] ?? "";
  const parts = slug.split("-");
  const maybeId = parts[parts.length - 1];
  if (/^\d+$/.test(maybeId ?? "")) parts.pop();
  const trimmed = parts.slice(-2);
  if (trimmed.length === 0) return slugToWords(slug);
  return slugToWords(trimmed.join("-"));
}

function searchPath(term: string | null): string {
  if (!term) return "/jobs/remote";
  const query = encodeURIComponent(term.trim());
  return `/jobs/remote?search=${query}`;
}

function matchesTerm(item: BuiltinListItem, term: string): boolean {
  const lower = term.toLowerCase();
  if (item.title.toLowerCase().includes(lower)) return true;
  if (item.description?.toLowerCase().includes(lower)) return true;
  return false;
}

async function fetchPage(path: string, page: number): Promise<string> {
  const separator = path.includes("?") ? "&" : "?";
  const url =
    page <= 1 ? `${ORIGIN}${path}` : `${ORIGIN}${path}${separator}page=${page}`;
  const response = await fetch(url, {
    headers: { Accept: "text/html", "User-Agent": "JobOps/1.0" },
  });
  if (!response.ok) {
    throw new Error(`Built In request failed (${response.status}) for ${url}`);
  }
  return response.text();
}

export const manifest: ExtractorManifest = {
  id: "builtin",
  displayName: "Built In",
  providesSources: ["builtin"],
  async run(context): Promise<ExtractorRunResult> {
    if (context.shouldCancel?.()) return { success: true, jobs: [] };

    const maxJobs = context.settings.builtinMaxJobsPerTerm
      ? Number.parseInt(context.settings.builtinMaxJobsPerTerm, 10)
      : 100;
    const cap = Number.isFinite(maxJobs)
      ? Math.min(Math.max(maxJobs, 1), 500)
      : 100;
    const maxPages = readMaxPages(context.settings.builtinMaxPagesPerTerm);

    const terms = context.searchTerms.length > 0 ? context.searchTerms : [null];

    const seen = new Set<string>();
    const out: CreateJobInput[] = [];

    try {
      for (let termIndex = 0; termIndex < terms.length; termIndex += 1) {
        if (context.shouldCancel?.()) break;
        const term = terms[termIndex];
        const path = searchPath(term);

        for (let page = 1; page <= maxPages; page += 1) {
          if (context.shouldCancel?.()) break;
          if (out.length >= cap) break;

          context.onProgress?.({
            phase: "list",
            termsProcessed: termIndex,
            termsTotal: terms.length,
            currentUrl: `${ORIGIN}${path}`,
            detail: `Built In: term ${termIndex + 1}/${terms.length}, page ${page}`,
          });

          const html = await fetchPage(path, page);
          const items = parseListItems(html);
          if (items.length === 0) break;

          for (const item of items) {
            if (out.length >= cap) break;
            if (term && !matchesTerm(item, term)) continue;

            const key = item.url;
            if (seen.has(key)) continue;
            seen.add(key);

            out.push({
              source: "builtin",
              sourceJobId: item.url.split("/").pop(),
              title: item.title,
              employer: employerFromUrl(item.url),
              jobUrl: item.url,
              applicationLink: item.url,
              location: "Remote",
              isRemote: true,
              jobDescription: item.description,
            });
          }
        }
      }

      context.onProgress?.({
        phase: "list",
        termsProcessed: terms.length,
        termsTotal: terms.length,
        currentUrl: `${ORIGIN}/jobs`,
        jobPagesProcessed: out.length,
        detail: `Built In: ${out.length} jobs`,
      });

      return { success: true, jobs: out };
    } catch (error) {
      const message = error instanceof Error ? error.message : "Unknown error";
      return { success: false, jobs: out, error: message };
    }
  },
};

export default manifest;
