/**
 * Factorial HR public career sites — tenant HTML job_posting pages.
 *
 * https://{tenant}.factorialhr.com/
 */

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

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

function readTenants(raw: string | undefined): string[] {
  if (!raw) return [];
  try {
    const parsed = JSON.parse(raw);
    if (Array.isArray(parsed)) {
      return parsed
        .map((entry) => (typeof entry === "string" ? entry.trim() : ""))
        .filter(Boolean);
    }
  } catch {
    // fall through
  }
  return raw
    .split(/[\n,;|]+/)
    .map((entry) => entry.trim())
    .filter(Boolean);
}

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

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

function parseJobLinks(html: string, origin: string): string[] {
  const links = new Set<string>();
  for (const match of html.matchAll(
    /(?:href|data-job-postings-url)=['"]([^'"]*\/job_posting\/[^'"]+)['"]/g,
  )) {
    const raw = match[1];
    const path = raw.startsWith("http")
      ? raw.replace(origin, "")
      : raw.startsWith("/")
        ? raw
        : `/${raw}`;
    links.add(path);
  }
  return [...links];
}

function parseJobDetail(html: string): {
  title?: string;
  description?: string;
} {
  const titleMatch =
    html.match(/property='og:title'[^>]*content='([^']+)'/) ??
    html.match(/<h1[^>]*>([^<]+)<\/h1>/);
  const bodyMatch = html.match(
    /<div class='mb-12'>[\s\S]*?<div class='mb-2 sm:mb-4'>[\s\S]*?<\/h1>([\s\S]*?)<\/div>\s*<\/div>/,
  );

  return {
    title: titleMatch?.[1]?.trim(),
    description: bodyMatch?.[1] ? stripHtml(bodyMatch[1]) : undefined,
  };
}

function matchesTerm(values: string[], term: string): boolean {
  const lower = term.toLowerCase();
  return values.some((value) => value.toLowerCase().includes(lower));
}

async function fetchText(url: string): Promise<string> {
  const response = await fetch(url, {
    headers: { Accept: "text/html", "User-Agent": "JobOps/1.0" },
  });
  if (!response.ok) {
    throw new Error(`Factorial request failed (${response.status}) for ${url}`);
  }
  return response.text();
}

function tenantOrigin(tenant: string): string {
  const host = tenant.includes(".") ? tenant : `${tenant}.factorialhr.com`;
  return host.startsWith("http") ? host : `https://${host}`;
}

function employerLabel(tenant: string, pageHtml: string): string {
  const ogSiteMatch = pageHtml.match(
    /property=['"]og:site_name['"][^>]*content=['"]([^'"]+)['"]/i,
  );
  if (ogSiteMatch?.[1]) {
    const cleaned = ogSiteMatch[1].split(/\s[-|–]\s/)[0]?.trim();
    if (cleaned) return cleaned;
  }

  return tenant
    .split(".")[0]
    .split(/[-_]/)
    .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
    .join(" ");
}

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

    const tenants = readTenants(context.settings.factorialTenants);
    if (tenants.length === 0) {
      return {
        success: true,
        jobs: [],
        error:
          "No Factorial tenants configured. Set FACTORIAL_TENANTS or factorialTenants (comma- or newline-separated subdomains, e.g. yourbourse).",
      };
    }

    const terms = context.searchTerms.length > 0 ? context.searchTerms : [];
    const seen = new Set<string>();
    const out: CreateJobInput[] = [];

    try {
      for (let i = 0; i < tenants.length; i += 1) {
        if (context.shouldCancel?.()) break;
        const tenant = tenants[i];
        const origin = tenantOrigin(tenant);

        context.onProgress?.({
          phase: "list",
          termsProcessed: i,
          termsTotal: tenants.length,
          currentUrl: origin,
          detail: `Factorial: ${tenant} (${i + 1}/${tenants.length})`,
        });

        const indexHtml = await fetchText(origin);
        const employer = employerLabel(tenant, indexHtml);
        const links = parseJobLinks(indexHtml, origin);

        for (const link of links) {
          if (context.shouldCancel?.()) break;

          const jobUrl = `${origin}${link}`;
          let title =
            link.split("/").pop()?.replace(/-\d+$/, "").replace(/-/g, " ") ??
            "Unknown Title";
          title = title
            .split(" ")
            .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
            .join(" ");
          let jobDescription: string | undefined;

          try {
            const detailHtml = await fetchText(jobUrl);
            const detail = parseJobDetail(detailHtml);
            if (detail.title) title = detail.title.replace(/\s+$/, "");
            if (detail.description) jobDescription = detail.description;
          } catch {
            // keep index-derived row when detail fetch fails
          }

          const haystack = [title, jobDescription ?? ""];
          if (
            terms.length > 0 &&
            !terms.some((term) => matchesTerm(haystack, term))
          ) {
            continue;
          }

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

          out.push({
            source: "factorial",
            sourceJobId: link.split("/").pop(),
            title,
            employer,
            jobUrl,
            applicationLink: jobUrl,
            location: "Unknown",
            jobDescription,
          });
        }
      }

      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;
