/* ========================================================================
   csv-batch.jsx — CSV upload, validation and the batch model (Story 6)
   Uploads are submitted, not applied: a batch is fulfilled by the
   Meaningful Planet team (or later, automation) and the platform is honest
   about the wait. Loads after estate.jsx.
   ======================================================================== */
const CB = window.MILO_DATA;
const { useState: uC, useEffect: eC } = React;

const ROW_CAP = 500;
const SERVICE_WINDOW = "Assignments are actioned within 48 hours. Larger batches can take up to 72 hours. If you have any questions, contact your account manager.";

/* ---------- Templates ---------- */
const TEMPLATES = {
  assign: {
    file: "milo-assign-sims-template.csv",
    columns: ["sim_serial", "full_name", "email"],
    sample: [["8944501912340002101", "Maya Rossi", "maya.rossi@example.co.uk"],
             ["8944501912340002102", "Tom Okafor", "tom.okafor@example.co.uk"]]
  },
  import: {
    file: "milo-import-users-template.csv",
    columns: ["full_name", "email"],
    sample: [["Maya Rossi", "maya.rossi@example.co.uk"],
             ["Tom Okafor", "tom.okafor@example.co.uk"]]
  }
};

function downloadCsv(name, rows) {
  const cell = (v) => { const s = String(v == null ? "" : v); return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s; };
  const body = rows.map((r) => r.map(cell).join(",")).join("\n");
  const url = URL.createObjectURL(new Blob([body], { type: "text/csv;charset=utf-8" }));
  const a = document.createElement("a");
  a.href = url; a.download = name;
  document.body.appendChild(a); a.click(); a.remove();
  setTimeout(() => URL.revokeObjectURL(url), 1000);
}
function downloadTemplate(kind) {
  const t = TEMPLATES[kind];
  downloadCsv(t.file, [t.columns].concat(t.sample));
}
/* A sample built from this estate's real records, so the demo runs live. */
function downloadSample(kind, scopeIds) {
  const t = TEMPLATES[kind];
  const names = [["Nina Halvorsen", "nina.halvorsen@acmeco.co.uk"], ["Owen Pryce", "owen.pryce@acmeco.co.uk"],
    ["Sofia Marchetti", "sofia.marchetti@acmeco.co.uk"], ["Ade Okonjo", "ade@acmeco.co.uk"],
    ["Rafael Santos", "rafael.santos@acmeco.co.uk"], ["Yusuf Karim", "yusuf.karim@acmeco.co.uk"],
    ["Marta Kowalska", "marta.kowalska@acmeco.co.uk"], ["Dylan Reeve", "dylan.reeve@acmeco.co.uk"],
    ["Aisha Bello", "aisha.bello@acmeco.co.uk"], ["Hugo Lindqvist", "hugo.lindqvist@acmeco.co.uk"],
    ["Leila Haddad", "leila.haddad@acmeco.co.uk"], ["Bruno Ferreira", "bruno.ferreira@acmeco.co.uk"],
    ["Chloe Duarte", "chloe.duarte@acmeco.co.uk"], ["Ismail Toure", "ismail.toure@acmeco.co.uk"],
    ["Freya Sandberg", "freya.sandberg@acmeco.co.uk"], ["Kenji Watanabe", "kenji.watanabe@acmeco.co.uk"],
    ["Rosa Delgado", "rosa.delgado@acmeco.co.uk"], ["Tobias Reinhardt", "tobias.reinhardt@acmeco.co.uk"],
    ["Amara Nwosu", "amara.nwosu@acmeco.co.uk"], ["Vikram Desai", "vikram.desai@acmeco.co.uk"]];
  if (kind === "import") return downloadCsv("milo-import-users-sample.csv", [t.columns].concat(names.slice(0, 6)));
  let sims = assignableSims();
  if (scopeIds && scopeIds.length) sims = sims.filter((s) => scopeIds.indexOf(s.id) !== -1);
  const rows = sims.slice(0, names.length).map((s, i) => [s.serial, names[i][0], names[i][1]]);
  /* Two rows are deliberately wrong, so the report has something to report.
     Both are malformed emails: a duplicate would block two rows, not one. */
  if (rows.length > 2) rows[2] = [rows[2][0], rows[2][1], "sofia.marchetti@acmeco"];
  if (rows.length > 5) rows[5] = [rows[5][0], rows[5][1], "yusuf.karim.acmeco.co.uk"];
  downloadCsv("milo-assign-sims-sample.csv", [t.columns].concat(rows));
}

/* ---------- Eligibility ----------
   CSV assignment covers UK and IoT SIMs only. International and travel SIMs
   are assigned by the team, so they are out of scope for this flow. */
function assignableSims() {
  const g = window.simGroup || ((s) => s.group || "individual");
  return CB.SIMS.filter((s) => {
    const grp = g(s);
    if (grp !== "pool" && grp !== "individual") return false;
    if (s.status === "Terminated") return false;
    return !CB.USERS.some((u) => u.sim_ids.includes(s.id));
  });
}

/* ---------- Parsing ---------- */
/* Enough CSV for the templates: quoted fields, escaped quotes, CRLF. */
function parseCsv(text) {
  const rows = [];
  let row = [], field = "", quoted = false;
  const src = text.replace(/^\uFEFF/, "");
  for (let i = 0; i < src.length; i++) {
    const c = src[i];
    if (quoted) {
      if (c === '"' && src[i + 1] === '"') { field += '"'; i++; }
      else if (c === '"') quoted = false;
      else field += c;
    } else if (c === '"') quoted = true;
    else if (c === ",") { row.push(field); field = ""; }
    else if (c === "\n" || c === "\r") {
      if (c === "\r" && src[i + 1] === "\n") i++;
      row.push(field); field = "";
      if (row.some((f) => f.trim() !== "")) rows.push(row);
      row = [];
    } else field += c;
  }
  row.push(field);
  if (row.some((f) => f.trim() !== "")) rows.push(row);
  return rows;
}

const EMAIL_OK = /^[^\s@]+@[^\s@]+\.[a-z]{2,}$/i;
const NAME_MAX = 100;

/* ---------- Validation ----------
   Returns either a structural failure (nothing to report on) or the three
   buckets. Client checks are a courtesy; the server owns truth. */
function validate(kind, text, scopeIds) {
  const t = TEMPLATES[kind];
  const grid = parseCsv(text);
  if (!grid.length) return { fatal: "That file is empty. Fill in the template and upload it again." };

  const header = grid[0].map((h) => h.trim().toLowerCase());
  const missing = t.columns.filter((c) => header.indexOf(c) === -1);
  if (missing.length) return {
    fatal: "That file is missing the " + missing.map((m) => "\u201c" + m + "\u201d").join(" and ") +
      " column" + (missing.length > 1 ? "s" : "") + ". Download the template and use its column headings.",
    hint: "Nothing has been created."
  };
  const extra = header.filter((h) => h && t.columns.indexOf(h) === -1);
  const body = grid.slice(1);
  if (!body.length) return { fatal: "That file has column headings but no rows." };
  if (body.length > ROW_CAP) return {
    fatal: "That file has " + body.length + " rows. The most we can take in one file is " + ROW_CAP + ".",
    hint: "Split it into smaller files and upload them one at a time."
  };

  const col = (name) => header.indexOf(name);
  const scope = scopeIds && scopeIds.length ? scopeIds : null;
  const pool = assignableSims();
  const seenEmail = {}, seenSerial = {};
  body.forEach((r) => {
    const em = (r[col("email")] || "").trim().toLowerCase();
    if (em) seenEmail[em] = (seenEmail[em] || 0) + 1;
    if (kind === "assign") {
      const sn = (r[col("sim_serial")] || "").trim();
      if (sn) seenSerial[sn] = (seenSerial[sn] || 0) + 1;
    }
  });

  const rows = body.map((r, i) => {
    const line = i + 2;
    const name = (r[col("full_name")] || "").trim();
    const email = (r[col("email")] || "").trim();
    const key = email.toLowerCase();
    const existing = CB.USERS.find((u) => u.email.toLowerCase() === key) || null;
    const out = { line, full_name: name, email, existing_user_id: existing ? existing.id : null };

    if (kind === "assign") {
      const serial = (r[col("sim_serial")] || "").trim();
      out.sim_serial = serial;
      const sim = CB.SIMS.find((s) => s.serial === serial) || null;
      out.sim_id = sim ? sim.id : null;
      if (!serial) return { ...out, state: "blocked", reason: "No SIM serial" };
      if (seenSerial[serial] > 1) return { ...out, state: "blocked", reason: "This SIM appears more than once in the file" };
      if (!sim) return { ...out, state: "blocked", reason: "No SIM with this serial" };
      if (pool.indexOf(sim) === -1) return { ...out, state: "blocked",
        reason: CB.USERS.some((u) => u.sim_ids.includes(sim.id)) ? "This SIM is already assigned" : "This SIM cannot be assigned from a file" };
      if (scope && scope.indexOf(sim.id) === -1) return { ...out, state: "blocked", reason: "This SIM is not in your selection" };
    }

    if (!name) return { ...out, state: "blocked", reason: "No full name" };
    if (name.length > NAME_MAX) return { ...out, state: "blocked", reason: "Full name is longer than " + NAME_MAX + " characters" };
    if (!email) return { ...out, state: "blocked", reason: "No email address" };
    if (!EMAIL_OK.test(email)) return { ...out, state: "blocked", reason: "That is not a valid email address" };
    if (seenEmail[key] > 1) return { ...out, state: "blocked", reason: "This email appears more than once in the file" };
    if (existing) return { ...out, state: "warning",
      reason: kind === "import" ? existing.full_name + " already has a record, so this row is skipped" :
        "The SIM will be assigned to " + existing.full_name + "'s existing record" };
    return { ...out, state: "ready" };
  });

  return {
    kind, extra,
    rows,
    ready: rows.filter((r) => r.state === "ready"),
    warnings: rows.filter((r) => r.state === "warning"),
    blocked: rows.filter((r) => r.state === "blocked")
  };
}
/* Warnings are submitted for assignment (the existing person is reused) and
   skipped for import (the person already exists). */
const submittable = (report) => report.kind === "import" ? report.ready : report.ready.concat(report.warnings);

/* ---------- Batch store ---------- */
const BATCH_KEY = "mp_batches_v1";
let BAT = { seq: 0, list: [] };
try { const raw = localStorage.getItem(BATCH_KEY); if (raw) BAT = { ...BAT, ...JSON.parse(raw) }; } catch (e) {}
const bSubs = new Set();
function bNotify() {
  try { localStorage.setItem(BATCH_KEY, JSON.stringify(BAT)); } catch (e) {}
  bSubs.forEach((cb) => cb());
}
function useBatches() {
  const [, force] = uC(0);
  eC(() => { const cb = () => force((n) => n + 1); bSubs.add(cb); return () => bSubs.delete(cb); }, []);
  return BAT;
}

function submitBatch(kind, rows) {
  BAT.seq += 1;
  const batch = {
    id: "b" + BAT.seq,
    ref: "BA-" + String(4300 + BAT.seq),
    kind,
    submitted_at: new Date().toISOString(),
    status: "submitted",
    rows: rows.map((r) => ({ ...r, state: "pending", reason: null }))
  };
  BAT.list.unshift(batch);
  bNotify();
  return batch;
}

const openBatches = () => BAT.list.filter((b) => b.status === "submitted");
/* A SIM waiting on a submitted batch, so every surface can say so. */
function pendingAssignment(simId) {
  for (const b of openBatches()) {
    const row = b.rows.find((r) => r.sim_id === simId && r.state === "pending");
    if (row) return { batch: b, row };
  }
  return null;
}
const firstOpenBatch = () => openBatches()[0] || null;

/* ---------- Fulfilment ----------
   In production this is the team or an automation marking rows complete. */
function nextUserId() { return Math.max(...CB.USERS.map((u) => u.id)) + 1; }
function initialsOf(name) {
  const p = name.trim().split(/\s+/);
  return ((p[0] || "")[0] + (p.length > 1 ? p[p.length - 1][0] : "")).toUpperCase();
}
function ensureUser(row) {
  if (row.existing_user_id) return CB.USERS.find((u) => u.id === row.existing_user_id) || null;
  const key = row.email.toLowerCase();
  const found = CB.USERS.find((u) => u.email.toLowerCase() === key);
  if (found) return found;
  /* A standard user: they hold SIMs, and have no access to the platform. */
  const user = { id: nextUserId(), full_name: row.full_name, email: row.email, initials: initialsOf(row.full_name), sim_ids: [] };
  CB.USERS.push(user);
  return user;
}
function fulfilBatch(batchId, failCount) {
  const b = BAT.list.find((x) => x.id === batchId);
  if (!b || b.status !== "submitted") return false;
  const pending = b.rows.filter((r) => r.state === "pending");
  const failing = pending.slice(-(failCount || 0));
  let created = 0, assigned = 0, emailed = 0;

  pending.forEach((row) => {
    if (failing.indexOf(row) !== -1) {
      row.state = "failed";
      row.reason = "The network rejected this assignment. Try again or contact your account manager.";
      return;
    }
    const before = CB.USERS.length;
    const user = ensureUser(row);
    if (!user) { row.state = "failed"; row.reason = "The person record could not be created."; return; }
    if (CB.USERS.length > before) created++;
    row.user_id = user.id;
    if (b.kind === "assign" && row.sim_id) {
      const sim = CB.SIMS.find((s) => s.id === row.sim_id);
      if (sim) {
        if (user.sim_ids.indexOf(sim.id) === -1) user.sim_ids.push(sim.id);
        sim.user_id = user.id;
        assigned++;
        /* eSIMs are emailed on completion, not on upload. */
        if (sim.sim_type === "eSIM" && window.MP_ESIM) { window.MP_ESIM.sendActivationEmail(sim.id); emailed++; }
      }
    }
    row.state = "done";
  });

  b.status = "complete";
  b.completed_at = new Date().toISOString();
  const failed = b.rows.filter((r) => r.state === "failed").length;
  const done = b.rows.filter((r) => r.state === "done").length;
  CB.ALERTS.unshift({
    id: Date.now(), kind: b.kind === "import" ? "users_imported" : "sims_assigned",
    title: b.kind === "import" ? "Your user import is complete" : "Your SIM assignments are complete",
    description: b.kind === "import" ?
      done + " user" + (done === 1 ? "" : "s") + " added" + (failed ? ", " + failed + " failed" : "") + ". Batch " + b.ref + "." :
      assigned + " SIM" + (assigned === 1 ? "" : "s") + " assigned" +
        (created ? ", " + created + " new user record" + (created === 1 ? "" : "s") + " created" : "") +
        (emailed ? ", " + emailed + " activation email" + (emailed === 1 ? "" : "s") + " sent" : "") +
        (failed ? ", " + failed + " failed" : "") + ". Batch " + b.ref + ".",
    time_ago: "Just now", batch_id: b.id
  });
  bNotify();
  return true;
}
function resetBatches() { BAT = { seq: 0, list: [] }; bNotify(); location.reload(); }

window.MP_BATCH = {
  TEMPLATES, ROW_CAP, SERVICE_WINDOW, downloadTemplate, downloadSample,
  validate, submittable, assignableSims,
  useBatches, submitBatch, pendingAssignment, firstOpenBatch, fulfilBatch, resetBatches,
  batchById: (id) => BAT.list.find((b) => b.id === id) || null,
  all: () => BAT.list.slice()
};
