/**
 * sync.js — dual-write layer (API vianexu + local mirror)
 *
 * Write:  API first → always local mirror
 *         If API fails → local only + add to queue
 * Read:   API first → fallback local (transparent)
 * Queue:  Background loop flushes pending writes when API recovers
 */

import { join }    from 'path';
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
import fetch        from 'node-fetch';

const BASE      = 'https://vianexu.com/clients/ecobe/admin/api';
const TIMEOUT   = 10000;
const BOT_TOKEN = () => process.env.BOT_API_TOKEN || '';

// ── Local paths ───────────────────────────────────────────────────────────────

function localDir(botDir, slug) {
  return join(botDir, 'mentees', slug);
}

function localPath(botDir, slug, file) {
  return join(localDir(botDir, slug), `${file}.json`);
}

function queuePath(botDir) {
  return join(botDir, 'sync-queue.json');
}

function ensureDir(dir) {
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
}

// ── Queue helpers ─────────────────────────────────────────────────────────────

function loadQueue(botDir) {
  try { return JSON.parse(readFileSync(queuePath(botDir), 'utf8')); } catch { return []; }
}

function saveQueue(botDir, queue) {
  writeFileSync(queuePath(botDir), JSON.stringify(queue, null, 2));
}

function enqueue(botDir, slug, file, data) {
  const queue = loadQueue(botDir);
  // Replace any existing pending entry for same slug+file
  const idx = queue.findIndex(e => e.slug === slug && e.file === file);
  const entry = { slug, file, data, queuedAt: new Date().toISOString() };
  if (idx >= 0) queue[idx] = entry; else queue.push(entry);
  saveQueue(botDir, queue);
}

// ── API helpers ───────────────────────────────────────────────────────────────

async function apiPut(slug, file, data) {
  const res = await fetch(`${BASE}/mentee/${slug}/${file}`, {
    method:  'PUT',
    headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${BOT_TOKEN()}` },
    body:    JSON.stringify(data),
    signal:  AbortSignal.timeout(TIMEOUT),
  });
  if (!res.ok) { const j = await res.json().catch(() => ({})); throw new Error(j.error || `PUT ${file} → ${res.status}`); }
  return true;
}

async function apiGet(slug, file) {
  const res = await fetch(`${BASE}/mentee/${slug}/${file}`, { signal: AbortSignal.timeout(TIMEOUT) });
  if (!res.ok) throw new Error(`GET ${file} → ${res.status}`);
  return res.json();
}

async function apiPost(slug, endpoint, body) {
  const res = await fetch(`${BASE}/mentee/${slug}/${endpoint}`, {
    method:  'POST',
    headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${BOT_TOKEN()}` },
    body:    JSON.stringify(body),
    signal:  AbortSignal.timeout(30000),
  });
  if (!res.ok) { const j = await res.json().catch(() => ({})); throw new Error(j.error || `POST ${endpoint} → ${res.status}`); }
  return true;
}

const CHUNK_SIZE = 60 * 1024; // 60KB per chunk (well under any body limit)

async function apiUploadBinary(slug, buffer, extra = {}) {
  const buf = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer, 'base64');
  const base64 = buf.toString('base64');
  const total = Math.ceil(base64.length / CHUNK_SIZE) || 1;
  const id = `${slug}-${Date.now()}`;
  const { ext, type } = extra;

  // Send chunks
  for (let i = 0; i < total; i++) {
    const chunk = base64.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);
    await apiPost(slug, 'upload-chunk', { id, index: i, total, ext, type, data: chunk });
  }
  // Finalize
  await apiPost(slug, 'upload-finalize', { id, ext, type });
}

// ── Public: create (new mentee) ───────────────────────────────────────────────

export async function syncCreate(botDir, folderKey, data) {
  // Always write local mirror first
  const dir = localDir(botDir, folderKey);
  ensureDir(join(dir, 'cvs'));
  writeFileSync(localPath(botDir, folderKey, 'career-strategy'), JSON.stringify(data, null, 2));

  // Push to API using PUT with folderKey as the slug — this works with both old and new
  // server code: the PUT handler creates the directory if it doesn't exist, so passing
  // the full slug_id ensures the server always creates the correctly named folder.
  try {
    await apiPut(folderKey, 'career-strategy', data);
    // Remove from queue if it was pending
    const queue = loadQueue(botDir);
    const filtered = queue.filter(e => !(e.slug === folderKey && e.file === '__create__'));
    if (filtered.length !== queue.length) saveQueue(botDir, filtered);
  } catch (e) {
    console.warn(`[sync] create failed for ${folderKey} — queued:`, e.message);
    enqueue(botDir, folderKey, '__create__', data);
  }
  return folderKey;
}

// ── Public: write (JSON) ──────────────────────────────────────────────────────

export async function syncWrite(botDir, slug, file, data) {
  // Always write local mirror first
  const dir = localDir(botDir, slug);
  ensureDir(dir);
  writeFileSync(localPath(botDir, slug, file), JSON.stringify(data, null, 2));

  // Then push to API; on failure, enqueue
  try {
    await apiPut(slug, file, data);
    // Remove from queue if it was there
    const queue = loadQueue(botDir);
    const filtered = queue.filter(e => !(e.slug === slug && e.file === file));
    if (filtered.length !== queue.length) saveQueue(botDir, filtered);
  } catch (e) {
    console.warn(`[sync] API write failed for ${slug}/${file} — queued:`, e.message);
    enqueue(botDir, slug, file, data);
  }
}

// ── Public: write (PDF) ───────────────────────────────────────────────────────

export async function syncWritePdf(botDir, slug, buffer) {
  // Local mirror
  const dir = localDir(botDir, slug);
  ensureDir(dir);
  writeFileSync(join(dir, 'report.pdf'), buffer);

  // API upload (base64 JSON)
  try {
    await fetch(`${BASE}/mentee/${slug}/report`, {
      method:  'POST',
      headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${BOT_TOKEN()}` },
      body:    JSON.stringify({ pdf: buffer.toString('base64') }),
      signal:  AbortSignal.timeout(30000),
    }).then(r => { if (!r.ok) throw new Error(`POST report → ${r.status}`); });
  } catch (e) {
    console.warn(`[sync] PDF upload failed for ${slug}:`, e.message);
    enqueue(botDir, slug, '__report_pdf__', null);
  }
}

// ── Public: write (CV binary) ─────────────────────────────────────────────────

export async function syncWriteCv(botDir, slug, cvType, ext, buffer) {
  // Local mirror (already done in documents handler — this just syncs to API)
  try {
    await apiUploadBinary(slug, buffer, { ext, type: cvType.replace('cv-', '') });
  } catch (e) {
    console.warn(`[sync] CV upload failed for ${slug}/${cvType}:`, e.message);
    // Queue as Buffer (will be re-read from local file on flush)
    enqueue(botDir, slug, `__cv_${cvType}_${ext}__`, null);
  }
}

// ── Public: read ──────────────────────────────────────────────────────────────

export async function syncRead(botDir, slug, file) {
  try {
    return await apiGet(slug, file);
  } catch (e) {
    console.warn(`[sync] API read failed for ${slug}/${file} — using local:`, e.message);
    const lp = localPath(botDir, slug, file);
    if (existsSync(lp)) return JSON.parse(readFileSync(lp, 'utf8'));
    return null;
  }
}

// ── Queue flush ───────────────────────────────────────────────────────────────

async function flushQueue(botDir) {
  const queue = loadQueue(botDir);
  if (!queue.length) return;

  const MAX_RETRIES = 5;
  const remaining = [];
  for (const entry of queue) {
    if ((entry.retries || 0) >= MAX_RETRIES) {
      console.warn(`[sync] dropping ${entry.slug}/${entry.file} after ${MAX_RETRIES} failed attempts`);
      continue;
    }
    const { slug, file, data } = entry;
    try {
      if (file === '__create__') {
        await apiPut(slug, 'career-strategy', data);
      } else if (file === '__report_pdf__') {
        const pdfPath = join(localDir(botDir, slug), 'report.pdf');
        if (!existsSync(pdfPath)) { console.warn(`[sync] report.pdf missing for ${slug}, dropping`); continue; }
        const buf = readFileSync(pdfPath);
        await fetch(`${BASE}/mentee/${slug}/report`, {
          method:  'POST',
          headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${BOT_TOKEN()}` },
          body:    JSON.stringify({ pdf: buf.toString('base64') }),
          signal:  AbortSignal.timeout(30000),
        }).then(r => { if (!r.ok) throw new Error(`${r.status}`); });
      } else if (file.startsWith('__cv_')) {
        const parts = file.replace(/^__cv_/, '').replace(/__$/, '').split('_');
        const ext   = parts.pop();
        const type  = parts.join('_').replace('cv-', '');
        // Re-read local file
        const cvFileName = type === 'personal' ? `cv-personal.${ext}` : `cv-linkedin.${ext}`;
        const cvPath = join(localDir(botDir, slug), cvFileName);
        if (!existsSync(cvPath)) { console.warn(`[sync] CV file missing for ${slug}/${file}, dropping`); continue; }
        const buf = readFileSync(cvPath);
        await apiUploadBinary(slug, buf, { ext, type });
      } else {
        await apiPut(slug, file, data);
      }
      console.log(`[sync] flushed ${slug}/${file}`);
    } catch (e) {
      console.warn(`[sync] flush failed for ${slug}/${file}:`, e.message);
      remaining.push({ ...entry, retries: (entry.retries || 0) + 1 });
    }
  }
  saveQueue(botDir, remaining);
}

// ── Start background sync loop ────────────────────────────────────────────────

export function startSyncLoop(botDir, intervalMs = 30000) {
  setInterval(() => flushQueue(botDir).catch(e => console.error('[sync] flush error:', e.message)), intervalMs);
  console.log(`[sync] loop started (every ${intervalMs / 1000}s)`);
}
