//12/1/2025 Allows Excel download and conversion of headers mapped to pb headers.
//coersion added to match pb field types. however, may still have difficulty in assimilating certain date formats from excel to pb date fields.
//due to Excel storing as all caps for boolean fields
// excelsync.js
import { FIELD_CONFIG } from "./fieldConfig.js";
import fs from "fs";
import axios from "axios";
import qs from "qs";
import * as XLSX from "xlsx";
import ExcelJS from "exceljs";
import dotenv from "dotenv";
import { serve } from "bun";
import PocketBase from "pocketbase"

dotenv.config();

// -----------------------------
// CONFIG FOR TOKEN ACQUISITION GRAPH API + POCKETBASE
// -----------------------------
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);
  fs.appendFileSync("excelsync.log", message + "\n");
}

// -----------------------------
// HEADER DETECTOR
// -----------------------------
//Gets the last column letter in the table converting from 0 index to excel actual column in letter form.
function columnLetter(index) {
  let letter = "";
  while (index >= 0) {
    letter = String.fromCharCode((index % 26) + 65) + letter;
    index = Math.floor(index / 26) - 1;
  }
  return letter;
}

// -----------------------------
// GRAPH TOKEN ACQUISITION
// -----------------------------
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;
}

// -----------------------------
// GRAPH DOWNLOADER HELPER
// -----------------------------
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",   // important: get raw binary
  });
  return Buffer.from(res.data);    // convert to Buffer for XLSX
}

// -----------------------------
// POCKETBASE LOGIN AND TOKEN ACQUISITION
// -----------------------------
async function getPocketBaseClient() {
  const pb = new PocketBase(pbUrl);
  await pb.collection("users").authWithPassword(pbEmail, pbPassword);
  log("✅ PocketBase token acquired");
  return pb;
}

// -----------------------------
// Get Data From Excel File
// -----------------------------

const HEADER_ROW_INDEX     = 2; // Row 3 in Excel
const DATA_START_ROW_INDEX = 3; // Row 4 in Excel → first data row

//clean and coerce Excel values to match PocketBase schema expectations
function cleanString(val) {
  if (val == null) return "";
  return String(val)
    .replace(/\u00A0/g, " ")   // normalize non-breaking spaces
    .replace(/[\r\n\t]/g, " ") // strip control chars
    .replace(/\s+/g, " ")      // collapse whitespace
    .trim();
}

function normalizeForPB(val, type) {
  const cleaned = cleanString(val);

  // Treat common placeholders as empty
  const emptyTokens = ["n/a", "na", "null", "none"];
  if (emptyTokens.includes(cleaned.toLowerCase())) {
    switch (type) {
      case "number": return 0;        // PocketBase defaults to 0
      case "bool":   return null;
      case "string": return null;
      case "email":  return null;     // email empty
      case "json":   return null;
      case "date":   return null;     // date empty
      default:       return null;
    }
  }

  if (cleaned === "") {
    switch (type) {
      case "number": return 0;
      case "bool":   return null;
      case "string": return null;
      case "email":  return null;
      case "json":   return null;
      case "date":   return null;
      default:       return null;
    }
  }

  switch (type) {
    case "bool":
    case "boolean": {
      const lower = cleaned.toLowerCase();
      if (["true","yes","y","1"].includes(lower)) return true;   // boolean true
      if (["false","no","n","0"].includes(lower)) return false;  // boolean false
      return null;
    }
    case "number": {
      const num = Number(cleaned);
      return Number.isNaN(num) ? 0 : num;
    }
    case "date": {
      const d = new Date(cleaned);
      return isNaN(d.getTime()) ? null : d.toISOString();
    }
    case "string":
    default:
      return cleaned;
  }
}

export function parseExcelFile(input) {
  let data;
  if (Buffer.isBuffer(input)) {
    data = input;
    log("Reading Excel file from Graph buffer");
  } else {
    data = fs.readFileSync(input);
    log(`Reading Excel file: ${input}`);
  }

  const workbook = XLSX.read(data, { type: "buffer", cellDates: true });
  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"] || [];

  // Helper to get value from cell or merged region
  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;
  };

  // 1. Read headers (Row 3)
  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() : "");
  }

  // 2. Build lookup map once
  const excelHeaderToConfig = Object.values(FIELD_CONFIG).reduce((map, cfg) => {
    if (cfg.excelHeader) {
      const key = cfg.excelHeader.trim();
      map[key] = cfg;
      map[key.toLowerCase()] = cfg;
    }
    return map;
  }, {});

  // 3. Rich header mapping
  const headerMapping = rawHeaders.map(raw => {
    const clean = raw.trim();
    const config = excelHeaderToConfig[clean] || excelHeaderToConfig[clean.toLowerCase()];

    return config
      ? { original: clean, pbField: config.pbField, type: config.type, isMapped: true, config }
      : { original: clean, pbField: "", type: "string", isMapped: false, isUnknown: true };
  });

  log(`Mapped ${headerMapping.filter(h => h.isMapped).length} columns`);

  // 4. Parse data rows (starting Row 4)
  const rows = [];
  for (let r = DATA_START_ROW_INDEX; r <= range.e.r; r++) {
    const values = [];
    let hasData = false;

    for (let c = range.s.c; c <= range.e.c; c++) {
      const val = getValue(r, c);
      values.push(val ?? null);
      if (val !== null && val !== "") hasData = true;
    }
    if (!hasData) continue;

    const rowObj = {};
    headerMapping.forEach((map, i) => {
      if (map.isMapped) {
        rowObj[map.pbField] = normalizeForPB(values[i], map.type);
      }
    });

    rows.push(rowObj);
  }

  return {
    data: rows,            // parsed row objects
    headerMapping,         // rich info about headers
    rawHeaders,            // original headers for debugging
    totalRows: rows.length // count of parsed rows
  };
}


// -----------------------------
// MAIN
// -----------------------------
let parsedRows = [];
let rawHeaders = [];
(async () => {
  try {
    log("🔵 Starting Excel Sync…");
    const graphToken = await getGraphToken();
    const pb = await getPocketBaseClient();
    // These can be update if the Excel File changes
    const driveId = "b!9YOqqr2xM0G2DFBwMEJYY4UzQgocFddEtpLYWuS9_AtkCKl2q8yjQJteQ7Ti4QSx";
    const itemId = "01SPNXLDSQTMG4F6BDANCLJU2FZKEDQLVU";
    const excelBuffer = await downloadExcelFromGraph(graphToken, driveId, itemId);
    const parsed = parseExcelFile(excelBuffer);
    parsedRows = parsed.data;
    rawHeaders = parsed.rawHeaders;
    // UPDATE AND LOG CHANGES TO POCKETBASE
    await syncRowsToPocketBase(pb, parsedRows);
    log(rawHeaders)
    log("✅ First part complete: tokens + Excel parsing ready");
  } catch (err) {
    log("❌ Sync failed: " + (err.message || err));
    process.exit(1);
  }
})();

// -------------------------
// Export to xlsx - Excel
// ---------------------------

// Toggle this flag to enable/disable the export server
const ENABLE_EXPORT = false;   // set to false to bypass download

function exportExcelforDL(parsedRows) {
  if (!parsedRows || parsedRows.length === 0) {
    throw new Error("⚠️ No rows to export");
  }

  const worksheet = XLSX.utils.json_to_sheet(parsedRows);

  // Ensure Job_Number is the first column
  const cols = Object.keys(parsedRows[0] || {});
  if (cols.includes("Job_Number")) {
    const ordered = ["Job_Number", ...cols.filter(c => c !== "Job_Number")];
    const remapped = parsedRows.map(r =>
      ordered.reduce((acc, key) => { acc[key] = r[key]; return acc; }, {})
    );
    const ws2 = XLSX.utils.json_to_sheet(remapped);
    Object.assign(worksheet, ws2);
  }

  const workbook = XLSX.utils.book_new();
  XLSX.utils.book_append_sheet(workbook, worksheet, "ParsedData");

  return XLSX.write(workbook, { type: "buffer", bookType: "xlsx" });
}

if (ENABLE_EXPORT) {
  const PORT = 3000;
  serve({
    port: PORT,
    fetch(req) {
      const url = new URL(req.url);

      if (url.pathname === "/") {
        const html = `
<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>ExcelSync Export</title></head>
<body>
  <h1>ExcelSync Export</h1>
  <p>Click below to download the parsed Excel file:</p>
  <button onclick="window.location.href='/download'">Download ParsedOutput.xlsx</button>
</body>
</html>`;
        return new Response(html, { headers: { "Content-Type": "text/html" } });
      }

      if (url.pathname === "/download") {
        try {
          const buffer = exportExcelforDL(parsedRows);
          return new Response(buffer, {
            headers: {
              "Content-Type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
              "Content-Disposition": "attachment; filename=ParsedOutput.xlsx"
            }
          });
        } catch (err) {
          return new Response("❌ Failed to generate Excel file: " + err.message, { status: 500 });
        }
      }

      return new Response("Visit / to open the export page.");
    }
  });

  log(`🚀 ExcelSync export server running at http://localhost:${PORT}/`);
  log(`👉 Open this URL in your browser to verify and download the parsed Excel file.`);
} else {
  log("⏭️ Export server bypassed (ENABLE_EXPORT=false)");
}

async function syncRowsToPocketBase(pb, parsedRows) {
  let changes = 0;

  for (const row of parsedRows) {
    const jobNumber = row.Job_Number;
    if (!jobNumber) continue;

    try {
      // Find existing record by Job_Number
      const records = await pb.collection(pbCollection).getFullList({
        filter: Number.isFinite(jobNumber)
          ? `Job_Number=${jobNumber}`
          : `Job_Number="${jobNumber}"`,
        batchSize: 1
      });

      if (records.length > 0) {
        const record = records[0];

        // Compare field-by-field
        const updatePayload = {};
        const changedFields = [];

        for (const [key, value] of Object.entries(row)) {
          // Look up the expected type from your header mapping
          const type = headerMapping.find(h => h.pbField === key)?.type || "string";

          if (!valuesEqual(record[key], value, type)) {
            updatePayload[key] = value;
            changedFields.push({
              field: key,
              before: record[key],
              after: value
            });
          }
        }

        if (changedFields.length > 0) {
          await pb.collection(pbCollection).update(record.id, updatePayload);

          // Log detailed changes
          const details = changedFields
            .map(f => `${f.field}: "${f.before}" → "${f.after}"`)
            .join("; ");
          log(`🔄 Updated Job_Number=${jobNumber}, changes: ${details}`);

          changes++;
        }
      } else {
        await pb.collection(pbCollection).create(row);
        log(`➕ Created new record for Job_Number=${jobNumber}`);
        changes++;
      }
    } catch (err) {
      log(`❌ Sync failed for Job_Number=${jobNumber}: ${err.message}`);
    }
  }

  log(`✅ Sync complete. Records added/updated: ${changes}`);
}
function valuesEqual(pbVal, excelVal, type) {
  if (pbVal == null && excelVal == null) return true;

  switch (type) {
    case "date": {
      const pbDate = new Date(pbVal);
      const excelDate = new Date(excelVal);
      return !isNaN(pbDate) && !isNaN(excelDate) && pbDate.getTime() === excelDate.getTime();
    }
    case "bool":
      return Boolean(pbVal) === Boolean(excelVal);
    case "number":
      return Number(pbVal) === Number(excelVal);
    default:
      const normalize = v => (v ?? "").toString().replace(/\s+/g, " ").trim();
      return normalize(pbVal) === normalize(excelVal);
  }
}

// -----------------------------
// UPDATE NOTES CELLS IN SHAREPOINT (REAL-TIME)
// -----------------------------
//function getNotesColumnLetterFromParsedHeaders(rawHeaders) {
//  const notesIndex = rawHeaders.findIndex(h => h.trim().toLowerCase() === "notes");
//  if (notesIndex === -1) {
//    log(`❌ 'Notes' header not found in HEADER_MAP keys: ${rawHeaders.join(", ")}`);
//    throw new Error("Notes column not found");
//  }
//  return columnLetter(notesIndex);
//}
//await updateNotesInSharePoint(graphToken, driveId, itemId, "Job Sheet", parsedRows, pb, excelBuffer);
// Update only changed Notes cells using workbook session + PATCH.
// This version relies solely on DATA_START_ROW_INDEX for row mapping.
//async function updateNotesInSharePoint(graphToken, driveId, itemId, worksheetName, parsedRows, pb, workbookBuffer) {
//  const wb = XLSX.read(workbookBuffer, { type: "buffer", cellDates: true });
//  const sheet = wb.Sheets[worksheetName];
//  if (!sheet) throw new Error(`Worksheet "${worksheetName}" not found`);

  // Column detection aligned to HEADER_MAP (same mapping used by parseExcelFile)
  //const notesColumnLetter = getNotesColumnLetterFromParsedHeaders(rawHeaders);
  //og(`✅ Notes column resolved to ${notesColumnLetter} via rawHeaders`);

  // Create a workbook session that persists changes
  //const sessionRes = await axios.post(
  //  `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/workbook/createSession`,
  //  { persistChanges: true },
  //  { headers: { Authorization: `Bearer ${graphToken}` } }
 // );
 // const sessionId = sessionRes.data.id;

 // let changes = 0;

  // Loop parsed rows; map i to Excel row using DATA_START_ROW_INDEX (zero-based)
//  for (let i = 0; i < parsedRows.length; i++) {
//    const row = parsedRows[i];
//    const jobNumber = row.Job_Number;
//    if (jobNumber == null || jobNumber === "") continue;

//    try {
//      const records = await pb.collection(pbCollection).getFullList({
//        filter: Number.isFinite(jobNumber) ? `Job_Number=${jobNumber}` : `Job_Number="${jobNumber}"`,
//        batchSize: 1
//      });
//      if (records.length === 0) continue;

      // Normalize notes for fair comparison
//      const normalize = v => (v ?? "").toString().replace(/\s+/g, " ").trim();
//      const pbNotes = normalize(records[0].Notes);
//      const excelNotes = normalize(row.Notes);

 //     if (pbNotes !== excelNotes) {
        // Excel uses 1-based row numbers; DATA_START_ROW_INDEX is zero-based
//       const excelRowNumber = DATA_START_ROW_INDEX + i + 1;
//        const cellAddress = `${notesColumnLetter}${excelRowNumber}`;

//        log(`✏️ Notes change for Job_Number=${jobNumber}`);
//        log(`   Before: "${excelNotes}"`);
//        log(`   After : "${pbNotes}"`);
//        log(`➡️ Writing to ${worksheetName}!${cellAddress}`);

//        await axios.patch(
//          `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/workbook/worksheets('${worksheetName}')/range(address='${cellAddress}')`,
//          { values: [[pbNotes]] },
//          {
//            headers: {
//              Authorization: `Bearer ${graphToken}`,
//              "Content-Type": "application/json",
//              "workbook-session-id": sessionId
//            }
//          }
//        );

//        row.Notes = pbNotes;
//        changes++;
//      }
//    } catch (err) {
//      log(`❌ Failed for Job_Number=${jobNumber}: ${err.message}`);
//    }
//  }

//  await axios.post(
//    `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/workbook/closeSession`,
//    {},
//    { headers: { Authorization: `Bearer ${graphToken}`, "workbook-session-id": sessionId } }
//  );

//  log(`✅ Notes-only sync complete. Changes applied: ${changes}`);
//}