/**
 * Cross-source duplicate detection.
 *
 * Two postings from different sources almost always describe the same role
 * when their employer + title agree once you strip noise (case, punctuation,
 * tracking suffixes, common decorations like "(Remote)", "- Toronto, ON" etc).
 * The fingerprint is intentionally coarse so we err on the side of skipping
 * a duplicate rather than re-showing it from a second source.
 */

const PUNCTUATION_RE = /[\p{P}\p{S}]+/gu;
const WHITESPACE_RE = /\s+/g;
const LEADING_NOISE_RE = /^(?:re-?post(?:ed)?|new|hot|urgent)\s*[-:]?\s*/i;
const PARENS_RE = /\s*[([][^)\]]*[)\]]/g;
// Trailing decorations we know are location / arrangement metadata, not role
// suffix. Matched after the title body and stripped before fingerprinting.
// Examples we want to strip:
//   "Software Engineer — Remote"
//   "Senior Engineer - Toronto, ON"
//   "Designer | Hybrid"
// Examples we must NOT strip (otherwise we'd collide unrelated roles):
//   "Etudiant – Stage" (Stage is a role qualifier in French postings)
//   "Designer — Senior" (level qualifier)
const TRAILING_LOCATION_KEYWORDS_RE =
  /\s+[-|–—]\s+(?:remote|hybrid|on[\s-]?site|wfh|telework|anywhere)\s*$/i;
// Escape the ASCII hyphen explicitly so it doesn't form a character range
// with the surrounding delimiters (which would silently swallow letters).
const TRAILING_CITY_REGION_RE = /\s+[-|–—]\s+[^,\-|–—]+,\s*[^,\-|–—]+\s*$/;
const COMPANY_LEGAL_SUFFIX_RE =
  /\b(?:inc|inc\.|ltd|ltd\.|llc|gmbh|s\.a\.|s\.r\.l|sa|nv|bv|plc|corp|corporation|co|company|holdings|holding)\b/g;

/** Hide rediscovered roles that match a prior skip/apply within this window. */
export const PRIOR_SKIP_DISMISS_LOOKBACK_MS = 90 * 24 * 60 * 60 * 1000;

function stripDiacritics(input: string): string {
  return input.normalize("NFKD").replace(/[\u0300-\u036f]/g, "");
}

export function normalizeEmployerForFingerprint(
  employer: string | null | undefined,
): string {
  if (!employer) return "";
  let value = stripDiacritics(employer.toLowerCase()).trim();
  value = value.replace(/^[^a-z0-9]+/g, "");
  value = value.replace(PARENS_RE, " ");
  value = value.replace(COMPANY_LEGAL_SUFFIX_RE, " ");
  value = value.replace(PUNCTUATION_RE, " ");
  value = value.replace(WHITESPACE_RE, "").trim();
  return value;
}

export function normalizeTitleForFingerprint(
  title: string | null | undefined,
): string {
  if (!title) return "";
  let value = stripDiacritics(title.toLowerCase()).trim();
  value = value.replace(LEADING_NOISE_RE, "");
  value = value.replace(PARENS_RE, " ");
  value = value.replace(TRAILING_LOCATION_KEYWORDS_RE, " ");
  value = value.replace(TRAILING_CITY_REGION_RE, " ");
  value = value.replace(PUNCTUATION_RE, " ");
  value = value.replace(WHITESPACE_RE, "").trim();
  return value;
}

/**
 * Build a stable, source-agnostic fingerprint for a posting.
 *
 * Returns `null` when employer or title is empty after normalization, so
 * callers fall back to URL/sourceJobId equality and don't accidentally
 * collapse unrelated rows under the empty key.
 */
export function buildJobContentFingerprint(args: {
  employer: string | null | undefined;
  title: string | null | undefined;
}): string | null {
  const employer = normalizeEmployerForFingerprint(args.employer);
  const title = normalizeTitleForFingerprint(args.title);
  if (!employer || !title) return null;
  return `${employer}::${title}`;
}

const DESCRIPTION_MIN_CHARS = 80;

export function normalizeDescriptionForFingerprint(
  jobDescription: string | null | undefined,
): string {
  if (!jobDescription?.trim()) return "";
  let value = stripDiacritics(jobDescription.toLowerCase());
  value = value.replace(/<[^>]+>/g, " ");
  value = value.replace(PUNCTUATION_RE, " ");
  value = value.replace(WHITESPACE_RE, " ").trim();
  if (value.length < DESCRIPTION_MIN_CHARS) return "";
  return value.slice(0, 400);
}

/**
 * Same employer + materially similar description (cross-posted copy).
 */
export function buildJobDescriptionFingerprint(args: {
  employer: string | null | undefined;
  jobDescription: string | null | undefined;
}): string | null {
  const employer = normalizeEmployerForFingerprint(args.employer);
  const description = normalizeDescriptionForFingerprint(args.jobDescription);
  if (!employer || !description) return null;
  return `${employer}::desc::${description}`;
}

/**
 * Whether two normalized employers refer to the same company for dismiss matching.
 * Allows prefix matches so `cgi` matches `cgiituklimited`.
 */
export function employersMatchForDismiss(
  normalizedEmployerA: string,
  normalizedEmployerB: string,
): boolean {
  if (!normalizedEmployerA || !normalizedEmployerB) return false;
  if (normalizedEmployerA === normalizedEmployerB) return true;
  const [short, long] =
    normalizedEmployerA.length <= normalizedEmployerB.length
      ? [normalizedEmployerA, normalizedEmployerB]
      : [normalizedEmployerB, normalizedEmployerA];
  if (short.length < 3) return false;
  return long.startsWith(short);
}

/**
 * Whether two normalized titles are the same role for dismiss matching.
 * Allows a trailing plural `s` variant (`engineer` vs `engineers`).
 */
export function titlesMatchForDismiss(
  normalizedTitleA: string,
  normalizedTitleB: string,
): boolean {
  if (!normalizedTitleA || !normalizedTitleB) return false;
  if (normalizedTitleA === normalizedTitleB) return true;
  if (
    normalizedTitleA.length > 4 &&
    normalizedTitleA.endsWith("s") &&
    normalizedTitleA.slice(0, -1) === normalizedTitleB
  ) {
    return true;
  }
  if (
    normalizedTitleB.length > 4 &&
    normalizedTitleB.endsWith("s") &&
    normalizedTitleB.slice(0, -1) === normalizedTitleA
  ) {
    return true;
  }
  return false;
}

/** Same company + same job title (normalized), for prior-skip hide rules. */
export function jobsMatchEmployerAndTitleDismiss(args: {
  employerA: string | null | undefined;
  titleA: string | null | undefined;
  employerB: string | null | undefined;
  titleB: string | null | undefined;
}): boolean {
  const employerA = normalizeEmployerForFingerprint(args.employerA);
  const employerB = normalizeEmployerForFingerprint(args.employerB);
  const titleA = normalizeTitleForFingerprint(args.titleA);
  const titleB = normalizeTitleForFingerprint(args.titleB);
  if (!employerA || !employerB || !titleA || !titleB) return false;
  return (
    employersMatchForDismiss(employerA, employerB) &&
    titlesMatchForDismiss(titleA, titleB)
  );
}

export function collectJobDedupKeys(args: {
  employer: string | null | undefined;
  title: string | null | undefined;
  jobDescription?: string | null | undefined;
}): string[] {
  const keys = new Set<string>();
  const content = buildJobContentFingerprint({
    employer: args.employer,
    title: args.title,
  });
  if (content) keys.add(content);
  const description = buildJobDescriptionFingerprint({
    employer: args.employer,
    jobDescription: args.jobDescription,
  });
  if (description) keys.add(description);
  return [...keys];
}
