// excelsync.js
// Excel → CSV → PocketBase sync by Job_Number.
// Raw strings only (true CSV), no coercion. Progress logs + robust error logging.

import { FIELD_CONFIG } from "./fieldConfig.js";
import fs from "fs";
import path from "path";
import axios from "axios";
import qs from "qs";
import * as XLSX from "xlsx";
import dotenv from "dotenv";
import PocketBase from "pocketbase";

dotenv.config();

// -----------------------------
// CONFIG
// -----------------------------
const CONFIG = JSON.parse(fs.readFileSync("config.json", "utf8"));
const tenantId = CONFIG.tenantId;
const clientId = CONFIG.clientId;
const pbUrl = CONFIG.pbUrl;
const pbCollection = CONFIG.pbCollection;
const pbEmail = CONFIG.pbEmail;
const clientSecret = process.env.CLIENT_SECRET;
const pbPassword = process.env.PB_PASSWORD;

// -----------------------------
// LOGGING
// -----------------------------
function log(message) {
  console.log(message);
  const logfile = path.resolve(process.cwd(), "excelsync.log");
  try {
    fs.appendFileSync(logfile, message + "\n", "utf8");
  } catch (e) {
    console.error("Log write failed:", e?.message || e);
  }
}

// -----------------------------
// GRAPH TOKEN + DOWNLOAD
// -----------------------------
async function getGraphToken() {
  const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
  const body = qs.stringify({
    client_id: clientId,
    client_secret: clientSecret,
    scope: "https://graph.microsoft.com/.default",
    grant_type: "client_credentials",
  });
  const res = await axios.post(tokenUrl, body, {
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
  });
  log("✅ Graph token acquired");
  return res.data.access_token;
}

async function downloadExcelFromGraph(graphToken, driveId, itemId) {
  const url = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/content`;
  const res = await axios.get(url, {
    headers: { Authorization: `Bearer ${graphToken}` },
    responseType: "arraybuffer",
  });
  return Buffer.from(res.data);
}

// -----------------------------
// POCKETBASE LOGIN
// -----------------------------
async function getPocketBaseClient() {
  const pb = new PocketBase(pbUrl);
  await pb.collection("users").authWithPassword(pbEmail, pbPassword);
  log("✅ PocketBase token acquired");
  return pb;
}
// -----------------------------
// Header mapping
// -----------------------------
function buildHeaderMapping(rawHeaders) {
  const excelHeaderToConfig = Object.values(FIELD_CONFIG).reduce((map, cfg) => {
    if (cfg.excelHeader) {
      const key = String(cfg.excelHeader).trim();
      map[key] = cfg;
      map[key.toLowerCase()] = cfg;
    }
    return map;
  }, {});
  return rawHeaders.map(raw => {
    const clean = String(raw || "").trim();
    const cfg = excelHeaderToConfig[clean] || excelHeaderToConfig[clean.toLowerCase()];
    return cfg ? { pbField: cfg.pbField, isMapped: true } : { pbField: "", isMapped: false };
  });
}

// -----------------------------
// Excel → CSV string
// -----------------------------
const HEADER_ROW_INDEX     = 2;
const DATA_START_ROW_INDEX = 3;

function excelToCSV(buffer) {
  const workbook = XLSX.read(buffer, { type: "buffer" });
  const sheet = workbook.Sheets["Job Sheet"];
  if (!sheet) throw new Error('Sheet named "Job Sheet" not found');

  const range  = XLSX.utils.decode_range(sheet["!ref"]);
  const merges = sheet["!merges"] || [];

  const getValue = (r, c) => {
    const addr = XLSX.utils.encode_cell({ r, c });
    const cell = sheet[addr];
    if (cell) return cell.v ?? cell.w ?? null;
    for (const m of merges) {
      if (m.s.r <= r && r <= m.e.r && m.s.c <= c && c <= m.e.c) {
        const master = sheet[XLSX.utils.encode_cell(m.s)];
        return master ? (master.v ?? master.w ?? null) : null;
      }
    }
    return null;
  };

  const rawHeaders = [];
  for (let c = range.s.c; c <= range.e.c; c++) {
    const val = getValue(HEADER_ROW_INDEX, c);
    rawHeaders.push(val ? String(val).trim() : "");
  }

  const headerMapping = buildHeaderMapping(rawHeaders);
  const pbHeaders = headerMapping.filter(h => h.isMapped).map(h => h.pbField);

  if (!pbHeaders.includes("Job_Number")) {
    throw new Error("Job_Number must be present in mapped headers.");
  }

  const rows = [];
  for (let r = DATA_START_ROW_INDEX; r <= range.e.r; r++) {
    const rowVals = [];
    let hasData = false;
    for (let c = range.s.c; c <= range.e.c; c++) {
      const v = getValue(r, c);
      if (v !== null && v !== "") hasData = true;
      rowVals.push(v === null ? "" : String(v));
    }
    if (!hasData) continue;
    rows.push(rowVals);
  }

  const escapeCSV = (s) => {
    const needsQuotes = /[",\n\r]/.test(s);
    const withDoubles = s.replace(/"/g, '""');
    return needsQuotes ? `"${withDoubles}"` : withDoubles;
  };

  const mappedColIndexes = headerMapping.map((h, idx) => (h.isMapped ? idx : -1)).filter(idx => idx !== -1);
  const headerLine = pbHeaders.map(h => escapeCSV(h)).join(",");
  const csvLines = [headerLine];

  for (const row of rows) {
    const mappedValues = mappedColIndexes.map(idx => escapeCSV(row[idx] || ""));
    csvLines.push(mappedValues.join(","));
  }

  const csv = csvLines.join("\n");
  log(`🧾 Built CSV with ${pbHeaders.length} columns and ${rows.length} data rows`);
  return { csv, headers: pbHeaders };
}
// -----------------------------
// Parse CSV string → array of objects
// -----------------------------
function parseCSV(csv) {
  const lines = csv.split(/\r?\n/).filter(l => l.length > 0);
  if (lines.length === 0) return { headers: [], rows: [] };

  const parseLine = (line) => {
    const out = [];
    let cur = "";
    let inQuotes = false;
    for (let i = 0; i < line.length; i++) {
      const ch = line[i];
      if (inQuotes) {
        if (ch === '"') {
          if (line[i + 1] === '"') { cur += '"'; i++; }
          else { inQuotes = false; }
        } else { cur += ch; }
      } else {
        if (ch === '"') inQuotes = true;
        else if (ch === ",") { out.push(cur); cur = ""; }
        else { cur += ch; }
      }
    }
    out.push(cur);
    return out;
  };

  const headers = parseLine(lines[0]).map(h => h.trim());
  const rows = [];
  for (let i = 1; i < lines.length; i++) {
    const cols = parseLine(lines[i]);
    const obj = {};
    for (let c = 0; c < headers.length; c++) {
      obj[headers[c]] = cols[c] ?? "";
    }
    rows.push(obj);
  }
  return { headers, rows };
}

// -----------------------------
// Error logging helper
// -----------------------------
// -----------------------------
// Error logging helper
// -----------------------------
function logPocketBaseError(err, context) {
  log(`❌ ${context}`);
  log(`Error message: ${err.message || "no message"}`);

  // Dump everything we can from the error object
  try {
    log(`Error dump: ${JSON.stringify(err, Object.getOwnPropertyNames(err), 2)}`);
  } catch {
    log("⚠️ Could not stringify error object");
  }

  // Axios attaches the server's JSON body here
  if (err.response) {
    log(`Response status: ${err.response.status} ${err.response.statusText || ""}`);
    if (err.response.data) {
      try {
        log(`Server response data: ${JSON.stringify(err.response.data, null, 2)}`);
      } catch {
        log("⚠️ Could not stringify response.data");
      }
    }
  }

  // PocketBase client sometimes attaches .data separately
  if (err.data) {
    try {
      log(`Server details: ${JSON.stringify(err.data, null, 2)}`);
    } catch {}
  }
}


// -----------------------------
// RESYNC: by Job_Number
// -----------------------------
// -----------------------------
// RESYNC: by Job_Number
// -----------------------------
async function resyncPocketBase(pb, csvRows) {
  log(`🔄 Starting resync for ${csvRows.length} rows...`);

  let existing = [];
  try {
    existing = await pb.collection(pbCollection).getFullList();
  } catch (err) {
    logPocketBaseError(err, "Failed to load existing records");
  }

  const jobMap = {};
  for (const rec of existing) {
    jobMap[rec.Job_Number] = rec.id;
  }

  const allowedFields = new Set(
    Object.values(FIELD_CONFIG).map(cfg => cfg.pbField).filter(Boolean)
  );

  for (let i = 0; i < csvRows.length; i++) {
    const row = csvRows[i];

    // Progress every 250 records
    if (i > 0 && i % 250 === 0) {
      log(`⏳ Progress: processed ${i} of ${csvRows.length} records...`);
    }

    const jobNumber = row.Job_Number;
    if (!jobNumber || jobNumber === "") continue;

    // Build payload with only allowed fields
    const payload = {};
    for (const [k, v] of Object.entries(row)) {
      if (k === "Job_Number") continue;
      if (allowedFields.has(k)) payload[k] = v;
    }

    // Helper to log errors with server response
    const logPocketBaseError = (err, context) => {
      log(`❌ ${context}`);
      log(`Error message: ${err.message || "no message"}`);

      try {
        log(`Error dump: ${JSON.stringify(err, Object.getOwnPropertyNames(err), 2)}`);
      } catch {}

      if (err.response) {
        log(`Response status: ${err.response.status} ${err.response.statusText || ""}`);
        if (err.response.data) {
          try {
            log(`Server response data: ${JSON.stringify(err.response.data, null, 2)}`);
          } catch {
            log("⚠️ Could not stringify response.data");
          }
        } else {
          log("⚠️ No response.data present");
        }
      } else {
        log("⚠️ No err.response present");
      }

      if (err.data) {
        try {
          log(`Server details: ${JSON.stringify(err.data, null, 2)}`);
        } catch {}
      }
    };

    if (jobMap[jobNumber]) {
      const id = jobMap[jobNumber];
      try {
        await pb.collection(pbCollection).update(id, payload);
      } catch (err) {
        logPocketBaseError(err, `Update failed for id=${id} Job_Number=${jobNumber}`);
      }
    } else {
      const createPayload = { Job_Number: jobNumber, ...payload };
      try {
        await pb.collection(pbCollection).create(createPayload);
      } catch (err) {
        logPocketBaseError(err, `Create failed for Job_Number=${jobNumber}`);
      }
    }
  }

  log("✅ Resync complete.");
}
