// MaxAMove — Money helpers (single source of truth for revenue + profit)
//
// ─── WHY THIS FILE EXISTS ────────────────────────────────────────────────────
// Revenue and profit math used to be duplicated across crm/dashboard.jsx and
// crm/views.jsx. The two copies drifted: views.jsx's jobRevenue() forgot to
// add tips, so Reports tab showed lower revenue than Dashboard for the same
// month. This file is the ONE place those formulas live. Both surfaces (and
// any future ones — Financials drill-downs, exports, etc.) call into here so
// they can never drift again.
//
// Exposes on window:
//   window.jobRevenue(j)                          → number (move + tip)
//   window.profitForJob(j, lead, settingsOpts)    → { revenue, costs, profit, margin, … }
//
// Why window globals instead of ES modules? The rest of this CRM loads JSX
// files via <script type="text/babel"> tags — there's no bundler, no import
// statements. Globals on window are the project's existing convention (see
// data.jsx, kpi.jsx, inventory_catalog.jsx).
//
// ─── REVENUE / TIP POLICY (Gavin-approved) ───────────────────────────────────
// Tips ARE gross revenue (full customer payment counts). When tips pass through
// to non-owner crew, that pass-through is a wage expense that reduces profit.
// Owner's tip share stays in profit with no offsetting expense.

(function () {
  'use strict';

  // ─── jobRevenue(j) ─────────────────────────────────────────────────────────
  // Returns the move's revenue (full customer payment) including tips.
  // Source precedence for the move portion:
  //   1. extra.actualCost.revenue  (set on Job Costs tab — authoritative)
  //   2. finalValue                (top-level cents/dollars set at close)
  //   3. extra.finalTotalCents     (older shape, divide by 100)
  //   4. j.value                   (contracted estimate — last-resort fallback)
  // Tip is always added on top from extra.tipTotal.
  function jobRevenue(j) {
    if (!j) return 0;
    const acRev = j.extra && j.extra.actualCost && Number(j.extra.actualCost.revenue);
    let move;
    if (acRev && acRev > 0) {
      move = acRev;
    } else if (j.finalValue != null && Number(j.finalValue) > 0) {
      move = Number(j.finalValue);
    } else {
      const cents = j.extra && (j.extra.finalTotalCents || j.extra.final_total_cents);
      move = cents ? Number(cents) / 100 : Number(j.value || 0);
    }
    const tipDollars = (j.extra && Number(j.extra.tipTotal)) || 0;
    return move + tipDollars;
  }

  // ─── profitForJob(j, lead, opts) ───────────────────────────────────────────
  // Returns { revenue, labor, tipPassThrough, materials, vehicle, other, miles,
  //          costs, profit, margin, fromActuals }
  //
  // opts must include:
  //   ownerCrewId  — string|null  the crew_member.id that maps to the owner
  //                  (resolved from settings.extra.ownerCrewId). Lets us tell
  //                  which tip share goes to profit vs. wage pass-through.
  //   crewRateMap  — { [crewId]: hourlyRate }   used only for the auto-cost
  //                  fallback when actualCost isn't filled in.
  //   perMileCost  — number       per-mile vehicle cost (e.g. 0.67), also only
  //                  used in the auto-cost fallback.
  //
  // Two paths:
  //   (a) If j.extra.actualCost is filled in (Gavin entered actuals on Job
  //       Costs), use that breakdown verbatim. ac.revenue is MOVE ONLY; tip
  //       is always added on top.
  //   (b) Otherwise estimate costs from crew × hours × rate + on-job expenses
  //       + estimated mileage × per-mile + lead acquisition cost.
  //
  // Tip handling is identical in both paths: owner's share flows to profit,
  // non-owner share is subtracted as `tipPassThrough` expense.
  function profitForJob(j, lead, opts) {
    opts = opts || {};
    const ownerCrewId = opts.ownerCrewId || null;
    const crewRateMap = opts.crewRateMap || {};
    const perMileCost = Number(opts.perMileCost) || 0;

    // Resolve tip dollars (preferred: extra.tipTotal; fallback: cents fields)
    const ac = j.extra && j.extra.actualCost;
    const tipFromDollars = j.extra && j.extra.tipTotal != null ? Number(j.extra.tipTotal) : null;
    const tipCents = (j.extra && (j.extra.tipCents || j.extra.tip_cents)) || j.tipCents || j.tip_cents;
    const tipDollars = tipFromDollars != null ? tipFromDollars : ((Number(tipCents) || 0) / 100);

    // Determine owner's share of tips
    const ownerKeepsAll = !!(j.extra && j.extra.tipExcludedFromCrew);
    const split = (j.extra && j.extra.crewTipSplit) || null;
    let ownerTipShare = 0;
    if (ownerKeepsAll) {
      ownerTipShare = tipDollars;
    } else if (split && ownerCrewId) {
      ownerTipShare = Number(split[ownerCrewId]) || 0;
    } else if (ownerCrewId) {
      const crewArr = Array.isArray(j.crewIds) ? j.crewIds : (Array.isArray(j.crew_ids) ? j.crew_ids : []);
      if (crewArr.length > 0 && crewArr.indexOf(ownerCrewId) !== -1) {
        ownerTipShare = tipDollars / crewArr.length;
      }
    }
    const tipPassThrough = Math.max(0, tipDollars - ownerTipShare);

    // ── Path A: actuals filled in (use ac.* verbatim) ──
    if (ac && (Number(ac.totalCost) > 0 || Number(ac.revenue) > 0)) {
      const moveRevenue = Number(ac.revenue) || 0;
      const revenue = moveRevenue + tipDollars;
      const labor = Number(ac.laborCost) || 0;
      const materials = Number(ac.materialsCost) || 0;
      const vehicle = Number(ac.mileageCost) || 0;
      const other = (Number(ac.leadCost) || 0) + (Number(ac.otherCost) || 0);
      const costs = labor + tipPassThrough + materials + vehicle + other;
      const profit = revenue - costs;
      const margin = revenue > 0 ? (profit / revenue) : 0;
      const miles = Number(ac.mileageMiles) || 0;
      return { revenue, labor, tipPassThrough, materials, vehicle, other, miles, costs, profit, margin, fromActuals: true };
    }

    // ── Path B: no actuals → auto-estimate ──
    const subtotalCents = j.extra && (j.extra.subtotalCents || j.extra.subtotal_cents);
    const subtotal = subtotalCents
      ? Number(subtotalCents) / 100
      : (j.finalValue != null && Number(j.finalValue) > 0 ? Number(j.finalValue) : Number(j.value || 0));
    const revenue = subtotal + tipDollars;

    // Labor — sum of hourly rate × hours per crew member
    const hours = Number((j.extra && j.extra.actualBillableHours) || 0);
    const crewIds = Array.isArray(j.crewIds) ? j.crewIds : (Array.isArray(j.crew_ids) ? j.crew_ids : []);
    const crewHoursOverride = (j.extra && j.extra.crewHours) || {};
    let labor = 0;
    crewIds.forEach(function (id) {
      const rate = Number(crewRateMap[id] || 0);
      const h = Number(crewHoursOverride[id] || hours || 0);
      labor += rate * h;
    });

    // Materials — sum of on-job expenses entered into extra.expenses
    const expenses = Array.isArray(j.extra && j.extra.expenses) ? j.extra.expenses : [];
    const materials = expenses.reduce(function (s, e) { return s + (Number(e.amount) || 0); }, 0);

    // Vehicle — distance from estimate × 2 (round trip) × per-mile rate
    let miles = 0;
    const dist = j.estimateMoveData && j.estimateMoveData.distance;
    if (dist) {
      const n = Number(String(dist).replace(/[^0-9.]/g, ''));
      if (isFinite(n) && n > 0) miles = n * 2;
    }
    const vehicle = miles * perMileCost;

    // Lead cost — what we paid to acquire this customer (Thumbtack click, etc.)
    const leadCost = Number(lead && lead.costOfLead) || 0;
    const other = leadCost;

    const costs = labor + tipPassThrough + materials + vehicle + other;
    const profit = revenue - costs;
    const margin = revenue > 0 ? (profit / revenue) : 0;
    return { revenue, labor, tipPassThrough, materials, vehicle, other, miles, costs, profit, margin, fromActuals: false };
  }

  // ─── leadValue(lead, jobs) ─────────────────────────────────────────────────
  // Returns the dollar value to DISPLAY for a lead anywhere in the CRM.
  //
  // Rule: if the lead has a linked job AND that job has real numbers
  //   (status === 'completed' OR actuals filled in on Job Costs), return the
  //   authoritative jobRevenue (move + tip). Otherwise fall back to lead.value
  //   (the quoted estimate). This way, before-the-job screens still show the
  //   quote, and after-the-job screens automatically swap in actuals — without
  //   any DB writes, migrations, or backfills.
  function leadValue(lead, jobs) {
    if (!lead) return 0;
    const list = Array.isArray(jobs) ? jobs : [];
    const linked = list.find(function (j) { return j && j.estimateLeadId === lead.id; });
    if (linked) {
      const ac = linked.extra && linked.extra.actualCost;
      const hasActuals = !!(ac && (Number(ac.revenue) > 0 || Number(ac.totalCost) > 0));
      if (hasActuals || linked.status === 'completed') {
        return jobRevenue(linked);
      }
    }
    return Number(lead.value) || 0;
  }

  // ─── customerRevenue(customer, jobs) ───────────────────────────────────────
  // Returns total lifetime revenue for a customer, summed from completed jobs
  // (move + tip per jobRevenue). Replaces the stored customers.total_spent
  // column, which was never wired to sync when actuals were entered on Job
  // Costs. Display-time computation — the customers row in Supabase is left
  // alone, so existing data is never overwritten.
  function customerRevenue(customer, jobs) {
    if (!customer) return 0;
    const cid = customer.id;
    if (!cid) return Number(customer.totalSpent) || 0;
    const list = Array.isArray(jobs) ? jobs : [];
    let total = 0;
    list.forEach(function (j) {
      if (!j || j.customerId !== cid) return;
      if (j.status !== 'completed') return;
      total += jobRevenue(j);
    });
    return total;
  }

  // ─── customerStats(customer, jobs) ─────────────────────────────────────────
  // Returns the live, computed-from-actuals snapshot for a customer:
  //   { revenue, jobCount, firstMove, lastMove, avgTicket, tipTotal,
  //     daysSinceLastMove, completedJobs: [{ date, revenue, tip, route,
  //                                          number, status, jobId }] }
  // Powers the Customers list + drawer so Total Jobs / Lifetime Value /
  // Last Move always match the jobs we actually completed for them. Stored
  // customers.* columns are NEVER modified — display-only.
  function customerStats(customer, jobs) {
    const empty = {
      revenue: 0, jobCount: 0, firstMove: null, lastMove: null,
      avgTicket: 0, tipTotal: 0, daysSinceLastMove: null, completedJobs: []
    };
    if (!customer) return empty;
    const cid = customer.id;
    if (!cid) return Object.assign({}, empty, {
      revenue: Number(customer.totalSpent) || 0,
      jobCount: Number(customer.jobCount || customer.jobs) || 0,
      lastMove: customer.lastMove || null,
    });
    const list = Array.isArray(jobs) ? jobs : [];
    const rows = [];
    list.forEach(function (j) {
      if (!j || j.customerId !== cid) return;
      if (j.status !== 'completed') return;
      const rev = jobRevenue(j);
      const tip = (j.extra && Number(j.extra.tipTotal)) || 0;
      const from = j.fromAddress || (j.estimateMoveData && j.estimateMoveData.fromAddress) || '';
      const to   = j.toAddress   || (j.estimateMoveData && j.estimateMoveData.toAddress)   || '';
      rows.push({
        jobId:   j.id,
        number:  j.number || '',
        date:    j.scheduledDate || j.date || null,
        revenue: rev,
        tip:     tip,
        from:    from,
        to:      to,
        status:  j.status,
      });
    });
    // Sort newest move first for drawer display
    rows.sort(function (a, b) {
      if (!a.date) return 1;
      if (!b.date) return -1;
      return b.date.localeCompare(a.date);
    });
    const revenue  = rows.reduce(function (s, r) { return s + r.revenue; }, 0);
    const tipTotal = rows.reduce(function (s, r) { return s + r.tip;     }, 0);
    const dates    = rows.map(function (r) { return r.date; }).filter(Boolean).sort();
    const firstMove = dates[0] || null;
    const lastMove  = dates[dates.length - 1] || null;
    const jobCount  = rows.length;
    const avgTicket = jobCount > 0 ? (revenue / jobCount) : 0;
    let daysSinceLastMove = null;
    if (lastMove) {
      const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(lastMove);
      const d = m ? new Date(+m[1], +m[2]-1, +m[3], 12, 0, 0) : new Date(lastMove);
      if (!isNaN(d.getTime())) {
        daysSinceLastMove = Math.floor((Date.now() - d.getTime()) / 86400000);
      }
    }
    return { revenue, jobCount, firstMove, lastMove, avgTicket, tipTotal, daysSinceLastMove, completedJobs: rows };
  }

  // Expose on window so dashboard.jsx + views.jsx + any future surface can use them
  window.jobRevenue = jobRevenue;
  window.profitForJob = profitForJob;
  window.leadValue = leadValue;
  window.customerRevenue = customerRevenue;
  window.customerStats = customerStats;
})();
