/**
 * groupJobs.js — Daily job dispatch to WhatsApp groups
 *
 * Every day, between 6:00–7:00 Swiss time, sends a curated job list
 * to each mentee group in "active" phase.
 *
 * Each group gets a deterministic random minute offset (based on slug)
 * so not all groups receive messages at the same second.
 *
 * Jobs sent are registered in mentees/{slug}/jobtracking.json
 */

import { existsSync, readFileSync, writeFileSync, readdirSync } from 'fs';
import { join } from 'path';
import { search, triage, searchParamsFromStrategy } from './jobSearch.js';
import { syncWrite } from './sync.js';
import { journeyJobsSent } from './journey.js';

// ── Swiss time helpers ─────────────────────────────────────────────────────────

function getSwissTime() {
  const str = new Date().toLocaleString('en-US', {
    timeZone: 'Europe/Zurich',
    hour: '2-digit', minute: '2-digit',
    hour12: false,
  });
  const [h, m] = str.split(':').map(Number);
  return { hour: h, minute: m };
}

function getSwissDateStr() {
  return new Date().toLocaleDateString('en-CA', { timeZone: 'Europe/Zurich' }); // YYYY-MM-DD
}

// Deterministic minute slot for a group (0–59) based on slug
function slugToMinute(slug) {
  let h = 5381;
  for (const c of slug) h = ((h << 5) + h) ^ c.charCodeAt(0);
  return Math.abs(h) % 60;
}

// ── Job Tracking ──────────────────────────────────────────────────────────────

function loadJobTracking(scriptDir, slug) {
  const p = join(scriptDir, 'mentees', slug, 'jobtracking.json');
  if (!existsSync(p)) return { slug, jobs: [] };
  try { return JSON.parse(readFileSync(p, 'utf8')); }
  catch { return { slug, jobs: [] }; }
}

function saveJobTracking(scriptDir, botDir, slug, jt) {
  const p = join(scriptDir, 'mentees', slug, 'jobtracking.json');
  writeFileSync(p, JSON.stringify(jt, null, 2));
  syncWrite(botDir, slug, 'jobtracking', jt).catch(e =>
    console.warn('[jobtrack] sync failed:', e.message)
  );
}

/**
 * Register sent jobs into jobtracking.json.
 * Returns array of job IDs added.
 */
function registerJobs(scriptDir, botDir, slug, jobs) {
  const jt        = loadJobTracking(scriptDir, slug);
  const existUrls = new Set((jt.jobs || []).map(j => j.url));
  const added     = [];
  const now       = new Date().toISOString();

  for (const job of jobs) {
    if (!job.url || existUrls.has(job.url)) continue;
    const id = `jt_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
    jt.jobs.push({
      id,
      title:               job.title,
      company:             job.company,
      location:            job.location,
      url:                 job.url,
      source:              job.source || 'search',
      foundAt:             now,
      sentToGroup:         true,
      sentAt:              now,
      // ── Client decision ────────────────────────────────────
      status:              'proposed',
      clientDecision:      null,
      clientDecidedAt:     null,
      clientRejectionReason: null,
      // ── Application pipeline ──────────────────────────────
      cvFile:              null,
      coverLetterFile:     null,
      appliedAt:           null,
      // ── Company response ──────────────────────────────────
      companyResponse:     null,
      companyResponseDate: null,
      companyResponseType: null,
      // ── Interview ─────────────────────────────────────────
      interviewScheduled:  false,
      interviewDate:       null,
      interviewType:       null,
      interviewPreparationDone: false,
      interviewPreparationNotes: null,
      interviewResponse:   null,
      interviewResponseDate: null,
      // ── Offer ─────────────────────────────────────────────
      offerReceived:       false,
      offerDetails:        null,
      // ── Closure ───────────────────────────────────────────
      closedAt:            null,
      closedReason:        null,
      notes:               '',
    });
    existUrls.add(job.url);
    added.push(id);
  }

  jt.updatedAt = now;
  saveJobTracking(scriptDir, botDir, slug, jt);
  return added;
}

// ── Triage ordering ───────────────────────────────────────────────────────────

/**
 * Parse URLs from the triaged display text (lines like "🔗 https://...") and
 * return the jobs array sorted in the order those URLs appear.
 * Jobs whose URLs don't appear in the triaged text are appended at the end so
 * they can still be registered but won't interfere with numbered references.
 */
function orderedByTriaged(triagedText, allJobs) {
  const urlMatches = triagedText.match(/🔗\s*(https?:\/\/\S+)/g) || [];
  const displayUrls = urlMatches.map(l => l.replace(/^🔗\s*/, '').trim());

  const byUrl = new Map(allJobs.map(j => [j.url, j]));
  const seen  = new Set();
  const ordered = [];

  for (const url of displayUrls) {
    const job = byUrl.get(url);
    if (job && !seen.has(url)) { seen.add(url); ordered.push(job); }
  }

  // Append remaining jobs (not shown in triaged text) so they're still tracked
  for (const job of allJobs) {
    if (!seen.has(job.url)) ordered.push(job);
  }

  return ordered;
}

// ── Core: triage + register + send pre-fetched jobs to a group ───────────────

/**
 * Triage, register, and send an already-fetched jobs array to a client group.
 * Used by both the daily scheduler and the coach's manual SENDTOGROUP command.
 *
 * @param {object} sock
 * @param {string} scriptDir  - ecobot root
 * @param {string} botDir     - bots/BOTID/
 * @param {string} slug       - mentee slug
 * @param {object} group      - group.json content (must have groupJid, leadName)
 * @param {Array}  jobs       - raw job objects from search()
 * @param {Function} aiFn
 * @param {string} [greeting] - header line prefix (defaults to "Good morning")
 * @param {string} [exclusionNote] - extra triage exclusion rules
 * @returns {number} number of new jobs registered
 */
export async function sendJobsToGroup(sock, scriptDir, botDir, slug, group, jobs, aiFn, greeting = 'Good morning', exclusionNote = '', priorityCities = null) {
  const groupJid = group.groupJid;
  if (!groupJid) throw new Error('No groupJid on group record');
  if (!jobs.length) return 0;

  // Filter out jobs already sent to this client (dedup by URL before triage)
  const existingJt  = loadJobTracking(scriptDir, slug);
  const sentUrls    = new Set((existingJt.jobs || []).map(j => j.url).filter(Boolean));
  const newJobs     = jobs.filter(j => j.url && !sentUrls.has(j.url));
  if (!newJobs.length) {
    console.log(`[groupJobs] all jobs already sent to ${slug} — skipping`);
    return 0;
  }

  const strategy   = existsSync(join(scriptDir, 'mentees', slug, 'career-strategy.json'))
    ? JSON.parse(readFileSync(join(scriptDir, 'mentees', slug, 'career-strategy.json'), 'utf8'))
    : {};
  const { location, priorityCities: stratCities } = searchParamsFromStrategy(strategy);
  const cities = priorityCities || stratCities;

  const triaged = await triage(newJobs, greeting, location, aiFn, exclusionNote, cities);

  const addedIds = registerJobs(scriptDir, botDir, slug, orderedByTriaged(triaged, newJobs));
  journeyJobsSent(scriptDir, slug, addedIds.length, addedIds);

  const firstName = group.leadName?.split(' ')[0] || 'there';
  const header    = `🌅 *${greeting}, ${firstName}!*\n\nHere are job opportunities for you:\n\n`;
  const fullMsg   = header + triaged;
  await sock.sendMessage(groupJid, { text: fullMsg });

  // Save to group-chat.json
  const chatPath = join(scriptDir, 'mentees', slug, 'group-chat.json');
  let chat = [];
  if (existsSync(chatPath)) { try { chat = JSON.parse(readFileSync(chatPath, 'utf8')); } catch {} }
  chat.push({ role: 'assistant', text: fullMsg, ts: new Date().toISOString() });
  writeFileSync(chatPath, JSON.stringify(chat, null, 2));

  // Update group.json
  const groupPath    = join(scriptDir, 'mentees', slug, 'group.json');
  const updatedGroup = { ...group, lastJobsSentAt: new Date().toISOString() };
  writeFileSync(groupPath, JSON.stringify(updatedGroup, null, 2));
  syncWrite(botDir, slug, 'group', updatedGroup).catch(() => {});

  console.log(`[groupJobs] sent ${addedIds.length} jobs to group of ${slug}`);
  return addedIds.length;
}

// ── Per-group dispatch (search + send) ────────────────────────────────────────

export async function dispatchJobsToGroup(sock, scriptDir, botDir, slug, group, aiFn, opts = {}) {
  const groupJid = group.groupJid;
  if (!groupJid) return;

  // Load career strategy
  const csPath = join(scriptDir, 'mentees', slug, 'career-strategy.json');
  if (!existsSync(csPath)) {
    console.warn(`[groupJobs] no career-strategy for ${slug}`);
    return;
  }
  const strategy = JSON.parse(readFileSync(csPath, 'utf8'));
  const { roles, location, locationFallback, exclusions, priorityCities } = searchParamsFromStrategy(strategy);

  if (!roles.length) {
    console.warn(`[groupJobs] no target roles for ${slug}`);
    return;
  }

  console.log(`[groupJobs] searching for ${slug}: ${roles.slice(0, 3).join(' | ')} · ${location}`);

  let jobs = await search(roles, location, aiFn, 30, locationFallback);
  if (!jobs.length) {
    console.log(`[groupJobs] no results for ${slug}`);
    return;
  }

  // ECO-40: Silver users get a smaller batch (1–2 jobs/day) than Gold.
  // opts.maxJobs caps the slice before triage; Gold callers leave it unset
  // for the full batch.
  if (typeof opts.maxJobs === 'number' && opts.maxJobs > 0) {
    jobs = jobs.slice(0, opts.maxJobs);
  }

  const exclusionNote = exclusions.length
    ? `\n- EXCLUDE roles matching any of these: ${exclusions.join(', ')}. This is ABSOLUTE — never include a role whose title matches any exclusion, even if results are scarce.`
    : '';
  await sendJobsToGroup(sock, scriptDir, botDir, slug, group, jobs, aiFn, opts.greeting || 'Good morning', exclusionNote, priorityCities);
}

// ── Scheduler ─────────────────────────────────────────────────────────────────

/**
 * Start the daily job dispatch scheduler.
 * Checks every minute; dispatches to each active group in its assigned slot.
 *
 * @param {object} sock      - Baileys socket
 * @param {string} scriptDir - ecobot root dir
 * @param {string} botDir    - bots/BOTID/ dir
 * @param {Function} aiFn   - ai(system, messages, model) function
 */
export function startJobScheduler(sock, scriptDir, botDir, aiFn) {
  const dispatched = new Map(); // slug → date string (to avoid double-dispatch)

  function getActiveGroups() {
    const menteesDir = join(scriptDir, 'mentees');
    if (!existsSync(menteesDir)) return [];
    const results = [];
    for (const slug of readdirSync(menteesDir)) {
      const groupPath = join(menteesDir, slug, 'group.json');
      if (!existsSync(groupPath)) continue;
      let group;
      try { group = JSON.parse(readFileSync(groupPath, 'utf8')); }
      catch { continue; }
      if (group.phase !== 'active') continue;
      results.push({ slug, group });
    }
    return results;
  }

  function alreadySentToday(group, today) {
    const lastSentDay = group.lastJobsSentAt
      ? new Date(group.lastJobsSentAt).toLocaleDateString('en-CA', { timeZone: 'Europe/Zurich' })
      : null;
    return lastSentDay === today;
  }

  const PAUSE_FLAG = join(botDir, 'jobs-paused.flag');
  const isPaused   = () => existsSync(PAUSE_FLAG);

  // ── Catchup: send to any active group that missed today's window ──────────
  async function catchupMissed() {
    if (isPaused()) { console.log('[groupJobs] catchup skipped — paused'); return; }
    const { hour } = getSwissTime();
    const today = getSwissDateStr();

    // Only catch up if it's past the dispatch window (07:00+)
    if (hour < 7) return;

    for (const { slug, group } of getActiveGroups()) {
      if (alreadySentToday(group, today)) continue;
      if (dispatched.get(slug) === today) continue;

      dispatched.set(slug, today);
      const delayMs = Math.floor(Math.random() * 600_000); // 0–600 s
      const delaySec = Math.round(delayMs / 1000);
      console.log(`[groupJobs] catchup scheduled for ${slug} in ${delaySec}s (missed 06:xx window)`);
      setTimeout(() => {
        if (isPaused()) { console.log(`[groupJobs] catchup aborted for ${slug} — paused`); return; }
        dispatchJobsToGroup(sock, scriptDir, botDir, slug, group, aiFn).catch(e =>
          console.error(`[groupJobs] catchup failed for ${slug}:`, e.message)
        );
      }, delayMs);
    }
  }

  // Run catchup shortly after startup (give socket time to settle)
  setTimeout(catchupMissed, 10_000);

  // ── Silver scheduler: 08:00 send / 14:00 nudge / 18:00 nudge Zurich ──────
  // De-duped per slug+date+slot via the silverFired set.
  function getSilverGroups() {
    const menteesDir = join(scriptDir, 'mentees');
    if (!existsSync(menteesDir)) return [];
    const out = [];
    for (const slug of readdirSync(menteesDir)) {
      const groupPath = join(menteesDir, slug, 'group.json');
      const silverPath = join(menteesDir, slug, 'silver.json');
      if (!existsSync(groupPath) || !existsSync(silverPath)) continue;
      try {
        const group = JSON.parse(readFileSync(groupPath, 'utf8'));
        if (group.tier !== 'silver') continue;
        const silver = JSON.parse(readFileSync(silverPath, 'utf8'));
        out.push({ slug, group, silver });
      } catch {}
    }
    return out;
  }
  const silverFired = new Set(); // key = `${slug}|${date}|${slot}`
  setInterval(async () => {
    if (isPaused()) return;
    const { hour, minute } = getSwissTime();
    const today = getSwissDateStr();
    const slot = hour === 8 ? 8 : hour === 14 ? 14 : hour === 18 ? 18 : null;
    if (!slot) return;
    for (const { slug } of getSilverGroups()) {
      const targetMinute = slugToMinute(slug);
      if (minute !== targetMinute) continue;
      const key = `${slug}|${today}|${slot}`;
      if (silverFired.has(key)) continue;
      silverFired.add(key);
      console.log(`[silver] tick ${slot}:${String(targetMinute).padStart(2,'0')} for ${slug}`);
      try {
        const { tickSilverUser } = await import('./silverCourse.js');
        await tickSilverUser(scriptDir, slug, slot);
      } catch (e) {
        console.error(`[silver] tick failed for ${slug}:`, e.message);
      }
    }
  }, 60_000);

  // ── Silver daily jobs: 09:00 Zurich, cap 2/day per user (ECO-40) ─────────
  // Reuses the existing Gold dispatchJobsToGroup pipeline. Only fires for
  // Silver users at accessLevel='full', coachJoinedAt set, and
  // career-strategy.json present. Same per-slug minute offset to spread
  // load.
  const silverJobsFired = new Set(); // key = `${slug}|${date}`
  setInterval(async () => {
    if (isPaused()) return;
    const { hour, minute } = getSwissTime();
    if (hour !== 9) return;
    const today = getSwissDateStr();
    for (const { slug, group, silver } of getSilverGroups()) {
      const targetMinute = slugToMinute(slug);
      if (minute !== targetMinute) continue;
      // Gate: only send when the user is unlocked + has a coach + has a strategy
      if (silver.accessLevel !== 'full') continue;
      if (!silver.coachJoinedAt) continue;
      if (!existsSync(join(scriptDir, 'mentees', slug, 'career-strategy.json'))) continue;
      const key = `${slug}|${today}`;
      if (silverJobsFired.has(key)) continue;
      silverJobsFired.add(key);
      console.log(`[silver-jobs] dispatching to ${slug} at 09:${String(targetMinute).padStart(2,'0')} Zurich`);
      dispatchJobsToGroup(sock, scriptDir, botDir, slug, group, aiFn, { maxJobs: 2 }).catch(e =>
        console.error(`[silver-jobs] dispatch failed for ${slug}:`, e.message)
      );
    }
  }, 60_000);

  // ── Regular scheduler: 06:00–06:59 Zurich (Gold) ─────────────────────────
  setInterval(async () => {
    if (isPaused()) return;
    const { hour, minute } = getSwissTime();
    const today = getSwissDateStr();

    if (hour !== 6) return;

    for (const { slug, group } of getActiveGroups()) {
      if (alreadySentToday(group, today)) continue;
      if (dispatched.get(slug) === today) continue;

      const targetMinute = slugToMinute(slug);
      if (minute !== targetMinute) continue;

      dispatched.set(slug, today);
      console.log(`[groupJobs] dispatching to ${slug} at 06:${String(targetMinute).padStart(2, '0')} Zurich`);
      dispatchJobsToGroup(sock, scriptDir, botDir, slug, group, aiFn).catch(e =>
        console.error(`[groupJobs] dispatch failed for ${slug}:`, e.message)
      );
    }
  }, 60_000); // check every minute

  console.log('[groupJobs] scheduler started (Gold: 06:00 Zurich; Silver: 08/14/18 Zurich + 09:00 daily jobs)');
}
