/* ========================================================================
   bulk.jsx — Bulk SIM management (Story 5)
   Selection is a mode, entered deliberately per table. Actions match the
   group. Eligibility is stated at confirm, results are per line.
   Loads after estate.jsx and travel-plans.jsx.
   ======================================================================== */
const { useState: uK, useEffect: eK, useMemo: mK } = React;
const KD = window.MILO_DATA;

/* ---------- Selection store, one scope per table ---------- */
const SCOPES = {};
const kSubs = new Set();
const kNotify = () => kSubs.forEach((cb) => cb());
function scopeOf(key) {
  if (!SCOPES[key]) SCOPES[key] = { on: false, ids: [] };
  return SCOPES[key];
}
/* One handle per table: the filter-row button, the checkbox column and the
   bulk bar all read the same scope. */
function useBulkScope(key, rows, group) {
  const [, force] = uK(0);
  eK(() => { const cb = () => force((n) => n + 1); kSubs.add(cb); return () => kSubs.delete(cb); }, []);
  eK(() => () => { SCOPES[key] = { on: false, ids: [] }; }, [key]);
  const sc = scopeOf(key);
  const selectable = rows.filter((s) => isSelectable(s));
  const ids = sc.ids.filter((id) => selectable.some((s) => s.id === id));
  const sims = ids.map((id) => KD.SIMS.find((s) => s.id === id)).filter(Boolean);
  return {
    key, group, on: sc.on, ids, sims, selectable,
    enter() { sc.on = true; kNotify(); },
    exit() { sc.on = false; sc.ids = []; kNotify(); },
    clear() { sc.ids = []; kNotify(); },
    refresh() { kNotify(); },
    has: (id) => ids.indexOf(id) !== -1,
    toggle(id) { sc.ids = ids.indexOf(id) !== -1 ? ids.filter((x) => x !== id) : ids.concat([id]); kNotify(); },
    toggleAll(pageRows) {
      const pick = pageRows.filter(isSelectable).map((s) => s.id);
      const all = pick.every((id) => ids.indexOf(id) !== -1);
      sc.ids = all ? ids.filter((id) => pick.indexOf(id) === -1) : ids.concat(pick.filter((id) => ids.indexOf(id) === -1));
      kNotify();
    }
  };
}
/* Terminated SIMs are never selectable. */
const isSelectable = (sim) => window.MP_REQ.estStatus(sim) !== "Terminated";

/* ---------- Eligibility and actions per group ---------- */
const st = (sim) => window.MP_REQ.estStatus(sim);
const hasOpenRequest = (sim) => !!window.MP_REQ.openRequestFor(sim.id);
function recurringPlan(sim) {
  return window.MP_TRAVEL.plansFor(sim.id).find((p) => p.type === "recurring" && window.MP_TRAVEL.planStatus(p) === "active") || null;
}
function renewablePlan(sim) {
  const rows = window.MP_TRAVEL.plansFor(sim.id).filter((p) => p.type === "one_off");
  return rows[0] || null;
}

const BATCH_NOTE = "The Meaningful Planet team will action this request and update each SIM when it is complete. One support ticket covers all selected SIMs.";

/* Why a row was skipped, tested in order. The first match wins, so every
   skipped SIM lands in exactly one group and the modal can name them all. */
const SKIP_REASONS = [
  { id: "open_request", test: (s) => hasOpenRequest(s),
    one: "1 already has an open request", many: (n) => n + " already have an open request" },
  { id: "suspended", test: (s) => st(s) === "Suspended",
    one: "1 is already suspended", many: (n) => n + " are already suspended" },
  { id: "active", test: (s) => st(s) === "Active" || st(s) === "Activated",
    one: "1 is already active", many: (n) => n + " are already active" },
  { id: "not_active", test: () => true,
    one: "1 is not active yet", many: (n) => n + " are not active yet" }
];
/* Renewal actions fail for a reason of their own, not a status one. */
const PLAN_SKIP = {
  cancel_renewal: { one: "1 has no renewing plan", many: (n) => n + " have no renewing plan" },
  renew: { one: "1 has no plan to renew", many: (n) => n + " have no plan to renew" },
  unassign: { one: "1 is not assigned to anyone", many: (n) => n + " are not assigned to anyone" }
};
function skipSentence(action, skipped) {
  if (!skipped.length) return null;
  const own = PLAN_SKIP[action.id];
  const groups = [];
  if (own) {
    groups.push(skipped.length === 1 ? own.one : own.many(skipped.length));
  } else {
    /* Only test causes this action actually gates on, so no true-but-wrong
       reason is offered for a row skipped for a different one. */
    const gates = { suspend: ["suspended", "not_active"], reactivate: ["active", "not_active"],
      req_suspend: ["open_request", "suspended", "not_active"],
      req_reactivate: ["open_request", "active", "not_active"] }[action.id] || ["not_active"];
    const pool = SKIP_REASONS.filter((r) => gates.indexOf(r.id) !== -1);
    pool.forEach((r) => {
      const rows = skipped.filter((s) => pool.find((x) => x.test(s)) === r);
      if (rows.length) groups.push(rows.length === 1 ? r.one : r.many(rows.length));
    });
  }
  return groups.join(", ") + " and will not change";
}

const BULK_ACTIONS = {
  suspend: {
    label: "Suspend", icon: "circle-pause", cap: "sim.manage", groups: ["pool", "individual"],
    eligible: (s) => st(s) === "Active" || st(s) === "Activated",
    question: (n) => "Suspend " + n.eligible + " of " + n.total + " selected SIMs?",
    /* Same substance as the single-SIM modal: what happens, and what it costs. */
    body: (rows) => {
      const one = rows.length === 1;
      /* Pooled SIMs carry no per-SIM price, so the comparison would read £0. */
      const priced = rows.every((s) => (KD.PLANS[s.plan] || {}).price);
      const now = rows.reduce((a, s) => a + ((KD.PLANS[s.plan] || {}).price || 0), 0);
      return <>{one ? "The number stays yours" : "The numbers stay yours"}, but {one ? "its plan and all services are" : "their plans and all services are"} paused and {one ? "the SIM" : "the SIMs"} will stop working straight away. You'll pay <strong>£2/month</strong> {one ? "for it" : "for each of them"}{priced ? <> instead of <strong>£{now.toFixed(2)}/month</strong> in total</> : null}, and you can reactivate {one ? "it" : "them"} at any time.</>;
    },
    warn: "Anyone using these SIMs loses calls, texts and data immediately.",
    verb: "suspended",
    run: (s) => { s.status = "Suspended"; s.data_active = false; }
  },
  reactivate: {
    label: "Reactivate", icon: "circle-play", cap: "sim.manage", groups: ["pool", "individual"],
    eligible: (s) => st(s) === "Suspended",
    question: (n) => "Reactivate " + n.eligible + " of " + n.total + " selected SIMs?",
    verb: "reactivated",
    run: (s) => { s.status = "Active"; s.data_active = true; }
  },
  req_suspend: {
    label: "Request Suspension", icon: "circle-pause", cap: "sim.manage", groups: ["international", "altnet"],
    eligible: (s) => st(s) === "Active" && !hasOpenRequest(s),
    question: (n) => "Request suspension of " + n.eligible + " of " + n.total + " selected SIMs?",
    warn: BATCH_NOTE,
    verb: "requested", batch: "suspend"
  },
  req_reactivate: {
    label: "Request Reactivation", icon: "circle-play", cap: "sim.manage", groups: ["international", "altnet"],
    eligible: (s) => st(s) === "Suspended" && !hasOpenRequest(s),
    question: (n) => "Request reactivation of " + n.eligible + " of " + n.total + " selected SIMs?",
    warn: BATCH_NOTE,
    verb: "requested", batch: "reactivate"
  },
  cancel_renewal: {
    label: "Cancel Renewal", icon: "clock", cap: "sim.manage", groups: ["travel"],
    eligible: (s) => !!recurringPlan(s),
    question: (n) => "Stop renewal on " + n.eligible + " of " + n.total + " selected travel eSIMs?",
    warn: "Each plan will not renew. It stays active until the end of its own paid period.",
    verb: "stopped",
    run: (s) => { const p = recurringPlan(s); if (p) window.MP_TRAVEL.cancelRenewal(p.id); }
  },
  renew: {
    label: "Renew Plan", icon: "refresh-cw", cap: "sim.order", groups: ["travel"],
    eligible: (s) => !!renewablePlan(s),
    question: (n) => "Renew the current plan on " + n.eligible + " of " + n.total + " selected travel eSIMs?",
    charge: true,
    verb: "renewed",
    run: (s) => {
      const p = renewablePlan(s);
      if (p) window.MP_TRAVEL.purchase({ userId: s.user_id || (KD.USERS.find((u) => u.sim_ids.includes(s.id)) || {}).id, ref: p.catalogue_ref, tokens: [] });
    }
  },
  assign_csv: {
    label: "Assign From File", icon: "upload", cap: "sim.manage", groups: ["pool", "individual"],
    eligible: (s) => !KD.USERS.some((u) => u.sim_ids.includes(s.id)) && !window.MP_BATCH.pendingAssignment(s.id),
    direct: true, verb: "assigned"
  },
  unassign: {
    label: "Unassign", icon: "user-minus", cap: "sim.manage", groups: ["pool", "individual"],
    eligible: (s) => KD.USERS.some((u) => u.sim_ids.includes(s.id)),
    question: (n) => "Unassign " + n.eligible + " of " + n.total + " selected SIMs?",
    body: (rows) => <>{rows.length === 1 ? "The SIM keeps its number and plan" : "The SIMs keep their numbers and plans"}, and nothing is charged or cancelled. {rows.length === 1 ? "It" : "They"} will sit in your unassigned inventory until you assign {rows.length === 1 ? "it" : "them"} to someone else.</>,
    verb: "unassigned",
    run: (s) => {
      KD.USERS.forEach((u) => { const i = u.sim_ids.indexOf(s.id); if (i !== -1) u.sim_ids.splice(i, 1); });
      s.user_id = null;
    }
  },
  export: {
    label: "Export Selected", icon: "download", cap: "data.export", groups: ["pool", "individual", "altnet", "international", "travel", "mixed"],
    eligible: () => true, verb: "exported", direct: true
  }
};

function actionsFor(group) {
  return Object.keys(BULK_ACTIONS)
    .filter((k) => BULK_ACTIONS[k].groups.indexOf(group) !== -1 && MP_ALLOW(BULK_ACTIONS[k].cap))
    .map((k) => ({ id: k, ...BULK_ACTIONS[k] }));
}
const anyBulkAction = (group) => actionsFor(group).length > 0;
function split(action, sims) {
  const eligible = sims.filter((s) => action.eligible(s));
  return { eligible, skipped: sims.filter((s) => !action.eligible(s)), total: sims.length };
}
function renewTotal(sims) {
  return sims.reduce((a, s) => { const p = renewablePlan(s); return a + (p ? p.price : 0); }, 0);
}

/* ---------- CSV export ---------- */
const CSV_COLUMNS = {
  pool: ["SIM Serial", "SIM Type", "Bundle Type", "Status", "Assigned User", "Assigned Number", "Data Used (GB)"],
  individual: ["SIM Serial", "SIM Type", "Bundle Type", "Status", "Assigned User", "Assigned Number", "Data Plan", "Data Used (GB)", "Monthly Spend"],
  altnet: ["SIM Serial", "Network", "SIM Type", "Status", "Assigned User", "Assigned Number", "Data Plan", "Data Used (GB)", "Monthly Spend"],
  mixed: ["SIM Serial", "SIM Type", "Status", "Assigned User", "Assigned Number", "Group", "Data Used (GB)"],
  international: ["SIM Serial", "Type", "Status", "Assigned User", "Local Number", "Country", "Network", "Allowance (GB)", "Used (GB)"],
  travel: ["User", "Coverage", "Allowance (GB)", "Plan Type", "Used (GB)", "Status", "Expires or Renews"]
};
function csvRow(sim, group) {
  const user = KD.USERS.find((u) => u.sim_ids.includes(sim.id));
  const name = user ? user.full_name : "";
  const E = window.MP_ESTATE, T = window.MP_TRAVEL;
  if (group === "international") {
    const c = E.COUNTRIES[sim.country] || {};
    return [sim.serial, sim.sim_type, st(sim), name, sim.msisdn || "", c.name || "", sim.network || "", sim.allowance_gb, E.estGb(sim.used)];
  }
  if (group === "travel") {
    const r = T.travelSummary(sim);
    return [name, T.areaLabel(r.area || sim.coverage), r.allowance_gb, r.type === "recurring" ? "Recurring" : "One-Off",
      E.estGb(r.used), st(sim), r.awaiting ? "Awaiting first use" : E.fmtDate(r.expires || r.renews_on) || ""];
  }
  const plan = KD.PLANS[sim.plan] || {};
  const base = [sim.serial, sim.sim_type];
  if (group === "mixed") return base.concat([st(sim), name, sim.msisdn || "", sim.group || "UK", E.estGb(sim.used)]);
  const bundle = sim.group ? "" : "Pooled";
  if (group === "altnet") return [sim.serial, sim.network || "", sim.sim_type, st(sim), name, sim.msisdn || "", plan.name || "", E.estGb(sim.used), simMonthly(sim).toFixed(2)];
  if (group === "individual") return base.concat([bundle, st(sim), name, sim.msisdn || "", plan.name || "", E.estGb(sim.used), simMonthly(sim).toFixed(2)]);
  return base.concat([bundle, st(sim), name, sim.msisdn || "", E.estGb(sim.used)]);
}
function exportCsv(sims, group) {
  const cell = (v) => { const s = String(v == null ? "" : v); return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s; };
  const cols = CSV_COLUMNS[group] || CSV_COLUMNS.mixed;
  const body = [cols.join(",")].concat(sims.map((s) => csvRow(s, group).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 = "milo-" + group + "-sims.csv";
  document.body.appendChild(a);
  a.click();
  a.remove();
  setTimeout(() => URL.revokeObjectURL(url), 1000);
}

/* ---------- Running a batch ---------- */
/* Failures are per line. The demo fails a fixed subset so the state shows. */
function runBatch(action, sims, failing) {
  if (action.batch) {
    const ids = sims.map((s) => s.id);
    const failed = failing ? ids.filter((id, i) => i % 5 === 4) : [];
    const done = ids.filter((id) => failed.indexOf(id) === -1);
    if (done.length) window.MP_REQ.createBatchRequest(done, action.batch);
    return sims.map((s) => ({ sim: s, ok: failed.indexOf(s.id) === -1 }));
  }
  return sims.map((s, i) => {
    const ok = !(failing && i % 5 === 4);
    if (ok && action.run) action.run(s);
    return { sim: s, ok };
  });
}

/* ---------- Pieces ---------- */
function BulkToggleButton({ bulk }) {
  if (!anyBulkAction(bulk.group)) return null;
  if (bulk.on) return (
    <div className="mp-bulk-enter">
      <Button variant="secondary" size="sm" icon="check" onClick={bulk.exit}>Done</Button>
    </div>);
  return (
    <div className="mp-bulk-enter">
      <Button variant="secondary" size="sm" icon="list-checks" onClick={bulk.enter}>Select Multiple SIMs</Button>
    </div>);
}

function BulkCheckHead({ bulk, pageRows }) {
  const pick = pageRows.filter(isSelectable);
  const all = pick.length > 0 && pick.every((s) => bulk.has(s.id));
  return (
    <th className="mp-check-cell">
      <input type="checkbox" className="mp-check" checked={all} aria-label="Select all on this page"
        onChange={() => bulk.toggleAll(pageRows)} />
    </th>);
}
function BulkCheckCell({ bulk, sim }) {
  if (!isSelectable(sim)) return <td className="mp-check-cell" />;
  return (
    <td className="mp-check-cell">
      <input type="checkbox" className="mp-check" checked={bulk.has(sim.id)}
        aria-label={"Select " + (sim.msisdn || sim.serial)}
        onChange={() => bulk.toggle(sim.id)} />
    </td>);
}

function BulkConfirm({ action, sims, onClose, onRun }) {
  const n = split(action, sims);
  const total = action.id === "renew" ? renewTotal(n.eligible) : 0;
  return (
    <div className="mp-modal-overlay" onClick={onClose}>
      <div className="mp-modal" onClick={(e) => e.stopPropagation()} style={{ maxWidth: 470 }}>
        <div className="mp-modal-ic mp-modal-ic--neutral"><Icon name={action.icon} size={20} /></div>
        <h3 className="mp-modal-title">{action.question({ eligible: n.eligible.length, total: n.total })}</h3>
        {action.body ? <p className="mp-modal-text">{action.body(n.eligible)}</p> : null}
        {n.skipped.length ?
          <p className="mp-modal-sub"><strong>{skipSentence(action, n.skipped)}</strong>.</p> : null}
        {action.charge ?
          <p className="mp-modal-sub">You will be charged <strong>£{total}</strong> for {n.eligible.length} plan{n.eligible.length === 1 ? "" : "s"}. The charge applies immediately and appears on your invoice.</p> : null}
        {action.warn ?
        <div className="mp-wiz-note mp-wiz-note--warn">
          <Icon name="triangle-alert" size={15} />
          <span>{action.warn}</span>
        </div> : null}
        <div className="mp-modal-actions">
          <Button variant="secondary" onClick={onClose}>{action.id === "cancel_renewal" ? "Keep Renewals" : "Cancel"}</Button>
          <Button variant="primary" icon={action.icon} onClick={() => onRun(n.eligible)}>{action.label}</Button>
        </div>
      </div>
    </div>);
}

function BulkResults({ action, results, onClose, onRetry }) {
  const ok = results.filter((r) => r.ok);
  const bad = results.filter((r) => !r.ok);
  return (
    <div className="mp-modal-overlay" onClick={onClose}>
      <div className="mp-modal" onClick={(e) => e.stopPropagation()} style={{ maxWidth: 470 }}>
        <div className={"mp-modal-ic " + (bad.length ? "mp-modal-ic--clay" : "mp-modal-ic--mint")}>
          <Icon name={bad.length ? "triangle-alert" : "circle-check"} size={20} />
        </div>
        <h3 className="mp-modal-title">{ok.length} {action.verb}{bad.length ? " · " + bad.length + " failed" : ""}</h3>
        {bad.length ?
        <>
          <p className="mp-modal-sub">These SIMs did not change. Nothing else was affected.</p>
          <ul className="mp-fail-list">
            {bad.map((r) => <li key={r.sim.id}><span className="mp-mono">{r.sim.msisdn === "Data only" ? r.sim.serial : r.sim.msisdn}</span><span>{action.batch ? "The request could not be sent" : "Could not be reached"}</span></li>)}
          </ul>
        </> :
          <p className="mp-modal-sub">Every selected SIM was updated.</p>}
        <div className="mp-modal-actions">
          <Button variant="secondary" onClick={onClose}>Close</Button>
          {bad.length ? <Button variant="primary" icon="rotate-ccw" onClick={() => onRetry(bad.map((r) => r.sim))}>Retry Failed</Button> : null}
        </div>
      </div>
    </div>);
}

/* ---------- The sticky bar ---------- */
function BulkBar({ bulk, tweaks }) {
  const ctx = window.useRoles ? window.useRoles() : null;
  const [asking, setAsking] = uK(null);
  const [results, setResults] = uK(null);
  const sims = bulk.sims;
  const failing = tweaks && tweaks.bulk_fail === "some";
  /* An action every selected row fails is hidden, not shown then refused. */
  const actions = actionsFor(bulk.group).filter((a) => a.direct || split(a, sims).eligible.length > 0);

  const run = (action, rows) => {
    const res = runBatch(action, rows, failing);
    setAsking(null);
    setResults({ action, results: res });
    if (res.every((r) => r.ok)) bulk.clear();
    /* Batches mutate records directly, so every surface needs telling —
       including the partial-failure path, where the selection is kept. */
    else bulk.refresh();
  };

  /* The results panel outlives the selection, so it sits outside the bar. */
  return (
    <>
      {bulk.on && sims.length ?
      <div className="mp-bulk-wrap">
        <div className="mp-bulk-bar">
          <div className="mp-bulk-count"><Icon name="list-checks" size={17} /><strong>{sims.length}</strong> selected</div>
          <div className="mp-bulk-actions">
            {actions.map((a) =>
              <Button key={a.id} variant={a.id === "export" ? "secondary" : "primary"} size="sm" icon={a.icon}
                onClick={() => {
                  if (a.id === "assign_csv") {
                    const ids = split(a, sims).eligible.map((s) => s.id);
                    location.hash = "/sims/assign-csv?sims=" + ids.join(",");
                    return;
                  }
                  if (a.id === "export") {
                    exportCsv(sims, bulk.group);
                    if (ctx) ctx.toast({ kind: "ok", title: "Export ready", body: sims.length + " SIM" + (sims.length === 1 ? "" : "s") + " downloaded as a CSV file." });
                    return;
                  }
                  setAsking(a);
                }}>{a.label}</Button>)}
            <button type="button" className="mp-bulk-clear" onClick={bulk.clear}>Clear Selection</button>
          </div>
        </div>
      </div> : null}
      {asking ? <BulkConfirm action={asking} sims={sims} onClose={() => setAsking(null)} onRun={(rows) => run(asking, rows)} /> : null}
      {results ?
      <BulkResults action={results.action} results={results.results}
        onClose={() => setResults(null)}
        onRetry={(rows) => run(results.action, rows)} /> : null}
    </>);
}

Object.assign(window, {
  useBulkScope, BulkToggleButton, BulkCheckHead, BulkCheckCell, BulkBar,
  anyBulkAction, bulkExportCsv: exportCsv
});
