/**
 * jobSearch.js — Shared job search and triage logic
 *
 * Used by both the coach private-chat handler (index.js)
 * and the group daily job dispatch (groupJobs.js).
 */

import fetch from 'node-fetch';

const TS_API  = 'https://api.theirstack.com/v1/jobs/search';
const PHP_API = 'https://vianexu.com/clients/ecobe/jobsearch/index.php';

// Cache to avoid redundant AI calls for the same location string
const _locationCache = {};

export async function toCodes(loc, aiFn) {
  const key = loc.toLowerCase().trim();
  if (key in _locationCache) return _locationCache[key];

  const raw = await aiFn(
    'You are a geography resolver. Reply ONLY with a valid JSON array of ISO 3166-1 alpha-2 country codes — no explanation, no markdown.',
    [{
      role: 'user',
      content: `Return the ISO country codes for this location: "${loc}"\n\nRules:\n- Single country or city → its country code, e.g. ["PT"]\n- Region (Africa, Nordic, MENA, LATAM, Southeast Asia, etc.) → all countries in that region\n- "worldwide", "global", "remote" → [] (empty, no filter)\n- If unsure → []`,
    }],
    'gpt-4.1-mini'
  );

  let codes = null;
  try {
    const parsed = JSON.parse(raw.trim());
    if (Array.isArray(parsed) && parsed.length > 0) codes = parsed;
  } catch { /* fallback: no filter */ }

  _locationCache[key] = codes;
  return codes;
}

export async function tsSearch(titles, location, maxAgeDays, aiFn) {
  const codes = await toCodes(location, aiFn);
  const keys  = [process.env.THEIRSTACK_KEY, process.env.THEIRSTACK_KEY2].filter(Boolean);
  for (const key of keys) {
    try {
      const res = await fetch(TS_API, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${key}` },
        body: JSON.stringify({
          job_title_or: titles,
          ...(codes ? { job_country_code_or: codes } : {}),
          posted_at_max_age_days: maxAgeDays,
          limit: 10,
        }),
        signal: AbortSignal.timeout(15000),
      });
      if (res.status === 402 || res.status === 403) {
        console.warn(`[ts] key ${key.slice(0, 8)}... no credits`);
        continue;
      }
      if (!res.ok) return [];
      const data = await res.json();
      return (data.data || []).map(j => ({
        title:    j.job_title || '',
        company:  (typeof j.company === 'string' ? j.company : j.company?.name) || '',
        location: j.short_location || j.location || '',
        url:      j.url || j.final_url || '',
        postedAt: j.date_posted || '',
      }));
    } catch (e) { console.error('[ts]', e.message); }
  }
  return [];
}

export async function phpSearch(query, location, maxAgeDays) {
  try {
    const res = await fetch(
      `${PHP_API}?action=search&q=${encodeURIComponent(query)}&location=${encodeURIComponent(location)}&max_age_days=${maxAgeDays}`,
      { signal: AbortSignal.timeout(15000) }
    );
    if (!res.ok) return [];
    const data = await res.json();
    return (Array.isArray(data) ? data : data.jobs || []).map(j => ({
      title:    j.title    || '',
      company:  j.company  || '',
      location: j.location || '',
      url:      j.url      || '',
    }));
  } catch (e) { console.error('[php]', e.message); return []; }
}

export async function search(titles, location, aiFn, maxAgeDays = 30, locationFallback = null) {
  const arr = Array.isArray(titles) ? titles : [titles];

  // Run phpSearch for every role in parallel so we don't miss results when
  // TheirStack has no credits — then deduplicate across all sources.
  const [ts, ...phpResults] = await Promise.all([
    tsSearch(arr, location, maxAgeDays, aiFn),
    ...arr.map(role => phpSearch(role, location, maxAgeDays)),
  ]);

  const seen = new Set();
  const jobs = [];
  for (const j of [...ts, ...phpResults.flat()]) {
    const key = `${j.title}|${j.company}`.toLowerCase();
    if (j.title && !seen.has(key)) { seen.add(key); jobs.push(j); }
  }

  // If still empty and a broader fallback location is available, retry phpSearch
  if (!jobs.length && locationFallback && locationFallback !== location) {
    console.log(`[search] 0 results for "${location}" — retrying with fallback "${locationFallback}"`);
    const fallbackResults = await Promise.all(arr.map(role => phpSearch(role, locationFallback, maxAgeDays)));
    for (const j of fallbackResults.flat()) {
      const key = `${j.title}|${j.company}`.toLowerCase();
      if (j.title && !seen.has(key)) { seen.add(key); jobs.push(j); }
    }
  }

  console.log(`[search] "${arr.join('|')}" in "${location}" → ${jobs.length}`);
  return jobs;
}

export async function triage(jobs, originalMsg, location, aiFn, extraRules = '', priorityCities = null) {
  const list = jobs.slice(0, 40).map((j, i) =>
    `${i + 1}. ${j.title} — ${j.company} (${j.location})${j.postedAt ? ` [${j.postedAt}]` : ''}\nURL: ${j.url}`
  ).join('\n\n');

  const locationRule = (priorityCities && priorityCities.length > 1)
    ? `LOCATION: Accept ONLY roles clearly in or near ANY of these cities/regions: ${priorityCities.join(', ')}. Exclude roles outside these areas.`
    : `LOCATION: the user asked for "${location}". EXCLUDE any role located in a different city or region. Only include roles clearly in or near "${location}". If nothing matches, say so.`;

  return aiFn(
    `Job search assistant. From these ${Math.min(jobs.length, 40)} listings, pick the BEST 5 to 10 matches — quality over quantity.
Present each as (number sequentially 1, 2, 3... with no gaps):
1. [Title] — [Company] ([Location])
🔗 [URL exactly as given — never alter]
_[One sentence why it fits]_

Hard rules:
- ${locationRule}
- Number entries 1, 2, 3... consecutively — NEVER skip numbers.
- EXCLUDE any role with title/description entirely in German or requiring German as mandatory language.
- NEVER invent or modify URLs — copy them exactly.
- EXCLUSIONS ARE ABSOLUTE: Any job whose title matches an exclusion MUST be rejected — no exceptions, no "less ideal but kept", no "kept due to director level". Return fewer results rather than violating exclusions.
- QUANTITY: Return 5–10 results only. If fewer than 5 jobs satisfy ALL hard rules, return only those that match (even 0–3) and state clearly: "Only X roles matched all criteria."${extraRules}
- Reply in the same language as: "${originalMsg}".`,
    [{ role: 'user', content: `JOBS:\n\n${list}` }],
    'gpt-4.1-mini'
  );
}

/**
 * Extract search params from a career-strategy object.
 */
export function searchParamsFromStrategy(strategy) {
  const seg      = strategy.strategy?.segments?.[0] || {};
  const roles    = seg.targetRoles
    || strategy.primaryRoles
    || strategy.targetRoleFamilies
    || [];

  const priorityCities = strategy.priorityCities?.length ? strategy.priorityCities : null;

  const location = seg.targetGeographies?.[0]
    || priorityCities?.[0]
    || strategy.primaryCountry
    || strategy.location
    || 'Europe';
  const exclusions = strategy.exclusions || [];

  // Fallback to country when primary location is a city, so phpSearch can
  // widen the search if the city-level query returns no results.
  const locationFallback = strategy.primaryCountry && strategy.primaryCountry !== location
    ? strategy.primaryCountry
    : null;

  return { roles, location, locationFallback, exclusions, priorityCities };
}
