// ── Map: where the clients, deals and leads do business (src/geo.js) ───────
// Zoom and pan; businesses close together fold into a numbered bubble that splits as you zoom in. Click one for its
// details and everyone within 25, 50 or 100 miles (with a "good referral fit" when the trades go together), a state or
// an industry to filter. Opened from the top menu (under More on a phone) or "Show on map" in a file (#/map/lead:ID).
// Drawn with d3 on a map of the states (/crm/us-states.json, no outside map service); d3 loads the first time.

const MAP_KINDS = { client: "Client", deal: "Deal", lead: "Lead" };
const MAP_PAGE = { client: "clients", deal: "deals", lead: "leads" };
const MAP_PREC = { address: "Address on file", statement: "Address on bank statement", areacode: "Area code (approximate)" };
const MAP_INDUSTRIES = ["Construction & trades", "Restaurants & food", "Auto sales & repair", "Healthcare", "Trucking & logistics", "Retail", "Beauty & salons", "Professional services", "Other"];
const MAP_COLORS = { "Construction & trades": "#0B57C5", "Restaurants & food": "#D9480F", "Auto sales & repair": "#2B8A3E", "Healthcare": "#AE3EC9", "Trucking & logistics": "#E8890C",
  "Retail": "#0C8599", "Beauty & salons": "#C2255C", "Professional services": "#5C7CFA", "Other": "#7A879C" };
// Who tends to send work to whom.
const MAP_FITS = { "Construction & trades": ["Trucking & logistics", "Retail", "Professional services"], "Restaurants & food": ["Retail", "Trucking & logistics"], "Auto sales & repair": ["Trucking & logistics", "Professional services"],
  "Healthcare": ["Beauty & salons", "Professional services"], "Trucking & logistics": ["Construction & trades", "Auto sales & repair", "Restaurants & food"], "Retail": ["Restaurants & food", "Construction & trades"],
  "Beauty & salons": ["Healthcare", "Retail"], "Professional services": ["Construction & trades", "Healthcare", "Auto sales & repair"], "Other": [] };
const MAP_FIPS = { "01": "AL", "02": "AK", "04": "AZ", "05": "AR", "06": "CA", "08": "CO", "09": "CT", "10": "DE", "11": "DC", "12": "FL", "13": "GA", "15": "HI", "16": "ID", "17": "IL", "18": "IN", "19": "IA", "20": "KS", "21": "KY", "22": "LA", "23": "ME", "24": "MD", "25": "MA", "26": "MI", "27": "MN", "28": "MS", "29": "MO", "30": "MT", "31": "NE", "32": "NV", "33": "NH", "34": "NJ", "35": "NM", "36": "NY", "37": "NC", "38": "ND", "39": "OH", "40": "OK", "41": "OR", "42": "PA", "44": "RI", "45": "SC", "46": "SD", "47": "TN", "48": "TX", "49": "UT", "50": "VT", "51": "VA", "53": "WA", "54": "WV", "55": "WI", "56": "WY" };
const mapMiles = (a, b) => { const R = 3958.8, t = Math.PI / 180, dl = (b.lat - a.lat) * t, dg = (b.lng - a.lng) * t; const h = Math.sin(dl / 2) ** 2 + Math.cos(a.lat * t) * Math.cos(b.lat * t) * Math.sin(dg / 2) ** 2; return 2 * R * Math.asin(Math.sqrt(h)); };
const mapDest = (lat, lng, bearing, miles) => { const R = 3958.8, d = miles / R, b = bearing * Math.PI / 180, p1 = lat * Math.PI / 180, l1 = lng * Math.PI / 180;
  const p2 = Math.asin(Math.sin(p1) * Math.cos(d) + Math.cos(p1) * Math.sin(d) * Math.cos(b)); return [p2 * 180 / Math.PI, (l1 + Math.atan2(Math.sin(b) * Math.sin(d) * Math.cos(p1), Math.cos(d) - Math.sin(p1) * Math.sin(p2))) * 180 / Math.PI]; };

// d3 and topojson-client, once, from the same place as React (checked against their published hashes).
let mapLibs = null;
function loadMapLibs() {
  if (window.d3 && window.topojson) return Promise.resolve();
  if (mapLibs) return mapLibs;
  const add = (src, integrity) => new Promise((ok, bad) => { const s = document.createElement("script"); s.src = src; s.integrity = integrity; s.crossOrigin = "anonymous"; s.onload = ok; s.onerror = () => bad(new Error("The map couldn't load. Check the connection and try again.")); document.head.appendChild(s); });
  mapLibs = Promise.all([
    add("https://unpkg.com/d3@7.8.5/dist/d3.min.js", "sha384-su5kReKyYlIFrI62mbQRKXHzFobMa7BHp1cK6julLPbnYcCW9NIZKJiTODjLPeDh"),
    add("https://unpkg.com/topojson-client@3.1.0/dist/topojson-client.min.js", "sha384-Ukv1p/xTma6P4/2bY5KzWBw+ydSpXmhCMtyciIQVDJ1RmOxtCYNMF1uXT9T63H67"),
  ]).catch(err => { mapLibs = null; throw err; });
  return mapLibs;
}

function MapView({ user, toast, focus }) {
  const may = canOf(user);
  const [data, setData] = useState(null);
  const [error, setError] = useState("");
  const [ready, setReady] = useState(false);
  const [layers, setLayers] = useState({ client: true, deal: true, lead: true });
  const [ind, setInd] = useState("");
  const [st, setSt] = useState("");
  const [rad, setRad] = useState(50);
  const [q, setQ] = useState("");
  const [sel, setSel] = useState(null);
  const [status, setStatus] = useState(null);
  const [busy, setBusy] = useState("");
  const svgRef = useRef(null), boxRef = useRef(null), tipRef = useRef(null), mapRef = useRef(null);

  const load = useCallback(() => API.mapData().then(setData, e => setError(e.message)), []);
  const loadStatus = useCallback(() => { if (may.manage) API.mapStatus().then(setStatus, () => {}); }, [may.manage]);
  useEffect(() => { load(); loadStatus(); loadMapLibs().then(() => setReady(true), e => setError(e.message)); }, []);

  const placed = useMemo(() => ((data && data.points) || []).filter(p => p.lat != null), [data]);
  const needle = q.trim().toLowerCase();
  const visible = useMemo(() => placed.filter(p => layers[p.kind] && (!ind || p.industry === ind) && (!st || p.state === st)
    && (!needle || `${p.biz} ${p.name} ${p.label || ""}`.toLowerCase().includes(needle))), [placed, layers, ind, st, needle]);
  // Opened from a file ("Show on map"): that business, picked.
  useEffect(() => {
    if (!focus || !placed.length) return;
    const [kind, id] = String(focus).split(":");
    const p = placed.find(x => x.kind === kind && x.id === id);
    if (p) { setLayers(l => ({ ...l, [kind]: true })); setSel(p); if (mapRef.current) mapRef.current.zoomTo(p, 7); }
  }, [focus, placed.length, ready]);

  // ── The map itself: built once d3 has loaded; redrawn when what's shown changes ──
  useEffect(() => {
    if (!ready || !svgRef.current || mapRef.current) return;
    const d3 = window.d3, topojson = window.topojson, W = 975, H = 610;
    const svg = d3.select(svgRef.current), g = svg.append("g");
    const proj = d3.geoAlbersUsa().scale(1300).translate([W / 2, H / 2]), path = d3.geoPath();
    const statesG = g.append("g"), ringG = g.append("g"), ptsG = g.append("g");
    const m = { k: 1, proj, svg, g, statesG, ringG, ptsG, states: [] };
    m.px = () => 975 / Math.max(300, svgRef.current.getBoundingClientRect().width) / m.k;
    m.zoom = d3.zoom().scaleExtent([1, 40]).translateExtent([[0, 0], [W, H]]).on("zoom", (e) => { g.attr("transform", e.transform); m.k = e.transform.k; m.draw && m.draw(); });
    svg.call(m.zoom).on("dblclick.zoom", null);
    m.zoomTo = (p, s) => { const xy = proj([p.lng, p.lat]); if (xy) svg.transition().duration(450).call(m.zoom.transform, d3.zoomIdentity.translate(W / 2, H / 2).scale(Math.max(m.k, s || 6)).translate(-xy[0], -xy[1])); };
    m.zoomState = (f) => { const [[x0, y0], [x1, y1]] = path.bounds(f); const s = Math.min(12, .85 / Math.max((x1 - x0) / W, (y1 - y0) / H)); svg.transition().duration(450).call(m.zoom.transform, d3.zoomIdentity.translate(W / 2, H / 2).scale(s).translate(-(x0 + x1) / 2, -(y0 + y1) / 2)); };
    m.reset = () => svg.transition().duration(350).call(m.zoom.transform, d3.zoomIdentity);
    mapRef.current = m;
    fetch("/crm/us-states.json").then(r => r.json()).then(topo => {
      m.states = topojson.feature(topo, topo.objects.states).features;
      statesG.selectAll("path").data(m.states).join("path").attr("class", "map-state").attr("d", path)
        .on("click", (e, f) => { const ab = MAP_FIPS[f.id]; if (ab) { setSt(ab); m.zoomState(f); } });
      m.draw && m.draw();
    }).catch(() => setError("The map of the states couldn't load. Reload the page to try again."));
  }, [ready]);

  useEffect(() => {
    const m = mapRef.current;
    if (!m) return;
    const d3 = window.d3, tip = tipRef.current;
    const showTip = (e, text) => { const box = boxRef.current.getBoundingClientRect(); tip.textContent = text; tip.hidden = false; tip.style.left = (e.clientX - box.left) + "px"; tip.style.top = (e.clientY - box.top) + "px"; };
    m.statesG.selectAll("path").classed("on", f => MAP_FIPS[f.id] === st);
    m.draw = () => {
      const u = m.px(), r = 6 * u;
      const pts = visible.map(p => { const xy = m.proj([p.lng, p.lat]); return xy ? { p, x: xy[0], y: xy[1] } : null; }).filter(Boolean);
      // The business picked is drawn on its own; the rest fold into bubbles while zoomed out.
      const rest = pts.filter(o => !sel || o.p.kind !== sel.kind || o.p.id !== sel.id), mine = pts.find(o => sel && o.p.kind === sel.kind && o.p.id === sel.id);
      const groups = [];
      if (m.k >= 14) rest.forEach(o => groups.push({ x: o.x, y: o.y, items: [o.p] }));
      else for (const o of rest) {
        const c = groups.find(c => Math.hypot(c.x - o.x, c.y - o.y) < 26 * u);
        if (c) { c.items.push(o.p); c.x += (o.x - c.x) / c.items.length; c.y += (o.y - c.y) / c.items.length; } else groups.push({ x: o.x, y: o.y, items: [o.p] });
      }
      if (mine) groups.push({ x: mine.x, y: mine.y, items: [mine.p], picked: true });
      m.ptsG.selectAll("g").remove();
      for (const grp of groups) {
        const node = m.ptsG.append("g").attr("transform", `translate(${grp.x},${grp.y})`);
        if (grp.items.length > 1) {
          node.attr("class", "map-cl");
          node.append("circle").attr("r", (10 + Math.min(10, Math.sqrt(grp.items.length) * 2.5)) * u).attr("stroke-width", 2 * u);
          node.append("text").text(grp.items.length).style("font-size", (11.5 * u) + "px");
          node.on("click", () => m.svg.transition().duration(350).call(m.zoom.transform, d3.zoomIdentity.translate(975 / 2, 610 / 2).scale(Math.min(40, m.k * 2.6)).translate(-grp.x, -grp.y)))
            .on("mousemove", (e) => showTip(e, `${grp.items.length} businesses: click to zoom in`)).on("mouseleave", () => { tip.hidden = true; });
        } else {
          const p = grp.items[0], c = MAP_COLORS[p.industry] || MAP_COLORS.Other;
          node.attr("class", `map-pt${grp.picked ? " sel" : ""}`);
          node.append("circle").attr("r", p.kind === "lead" ? r * .9 : r).attr("fill", p.kind === "lead" ? "var(--paper)" : c).attr("stroke", grp.picked ? "var(--ink)" : p.kind === "deal" ? "var(--paper)" : c)
            .attr("stroke-width", (grp.picked ? 3 : p.kind === "deal" ? 2 : 1.8) * u).attr("stroke-dasharray", p.precision === "areacode" ? `${2.5 * u} ${2 * u}` : null);
          if (p.kind === "deal") node.append("circle").attr("r", r + 2 * u).attr("fill", "none").attr("stroke", c).attr("stroke-width", 1.5 * u);
          node.on("click", (e) => { e.stopPropagation(); setSel(p); })
            .on("mousemove", (e) => showTip(e, `${p.biz} · ${p.label || p.state || ""}`)).on("mouseleave", () => { tip.hidden = true; });
        }
      }
      m.ringG.selectAll("*").remove();
      if (sel && sel.lat != null) {
        const ring = d3.range(0, 361, 6).map(a => { const q2 = mapDest(sel.lat, sel.lng, a, rad); return m.proj([q2[1], q2[0]]); }).filter(Boolean);
        m.ringG.append("path").attr("class", "map-ring").attr("d", d3.line()(ring) + "Z").attr("stroke-width", 1.4 * u).attr("stroke-dasharray", `${5 * u} ${4 * u}`);
      }
    };
    m.draw();
    const onResize = () => m.draw();
    window.addEventListener("resize", onResize);
    return () => window.removeEventListener("resize", onResize);
  }, [ready, visible, sel, rad, st]);

  const pickState = (ab) => { setSt(ab); const m = mapRef.current; const f = m && m.states.find(s => MAP_FIPS[s.id] === ab); if (f) m.zoomState(f); else if (m) m.reset(); };
  const near = useMemo(() => {
    if (!sel) return [];
    return placed.filter(p => !(p.kind === sel.kind && p.id === sel.id) && layers[p.kind]).map(p => ({ p, d: mapMiles(sel, p) })).filter(o => o.d <= rad).sort((a, b) => a.d - b.d).slice(0, 60);
  }, [sel, placed, layers, rad]);
  const count = (kind) => placed.filter(p => p.kind === kind).length;
  const countRows = (key) => { const m = new Map(); visible.forEach(p => { const k = p[key] || "?"; m.set(k, (m.get(k) || 0) + 1); }); return [...m].sort((a, b) => b[1] - a[1]); };
  const byState = countRows("state").slice(0, 10), byInd = countRows("industry");
  const funded = visible.reduce((t, p) => t + (p.funded || 0), 0), withRev = visible.filter(p => p.revenue > 0);
  const avgRev = withRev.length ? withRev.reduce((t, p) => t + p.revenue, 0) / withRev.length : 0;
  const states = [...new Set(placed.map(p => p.state).filter(Boolean))].sort();
  const run = async (what) => {
    setBusy(what);
    try {
      if (what === "addresses") { const r = await API.mapFindAddresses(); toast(r.queued ? `Reading the address on ${r.queued} statements in the background. This box shows how many are left.` : "Already reading in the background"); }
      else { await API.mapLocate(); toast("Placing businesses on the map in the background"); setTimeout(load, 15000); }
      loadStatus();
    } catch (e) { toast(e.message, true); }
    setBusy("");
  };
  const located = data && data.located;
  const bar = (rows, onPick, colorOf) => { const max = Math.max(1, ...rows.map(r => r[1])); return rows.length ? rows.map(([k, n]) => (
    <div className="map-brow" key={k}><button type="button" onClick={() => onPick(k)} title={k}>{k}</button><div className="map-track"><div className="map-fill" style={{ width: `${(n / max) * 100}%`, background: colorOf ? colorOf(k) : "var(--blue)" }} /></div><span className="tnum">{n}</span></div>
  )) : <div className="dim" style={{ fontSize: 13 }}>Nothing to show.</div>; };

  return (
    <main className="workspace single">
      <div className="page map-page">
        <div className="page-head">
          <div><h1 className="page-title">Map</h1><p className="dim" style={{ margin: "2px 0 0" }}>Where your clients, deals and leads do business. Click a business to see who's nearby to refer.</p></div>
        </div>
        {error ? <div className="banner" role="alert">{error}</div> : null}
        <div className="map-bar" role="toolbar" aria-label="Map filters">
          <div className="seg" role="group" aria-label="Show">
            {["client", "deal", "lead"].map(kind => <button type="button" key={kind} aria-pressed={layers[kind]} onClick={() => setLayers({ ...layers, [kind]: !layers[kind] })}>{MAP_KINDS[kind]}s <span className="tnum" style={{ opacity: .75, fontWeight: 500 }}>{count(kind)}</span></button>)}
          </div>
          <select className="select sm" value={ind} onChange={e => setInd(e.target.value)} aria-label="Industry"><option value="">All industries</option>{MAP_INDUSTRIES.map(i => <option key={i}>{i}</option>)}</select>
          <select className="select sm" value={st} onChange={e => pickState(e.target.value)} aria-label="State"><option value="">All states</option>{states.map(s => <option key={s}>{s}</option>)}</select>
          <select className="select sm" value={rad} onChange={e => setRad(Number(e.target.value))} aria-label="Nearby distance"><option value={25}>Nearby: 25 miles</option><option value={50}>Nearby: 50 miles</option><option value={100}>Nearby: 100 miles</option></select>
          <input className="input sm map-search" value={q} onChange={e => setQ(e.target.value)} placeholder="Search a business or city" aria-label="Search a business or city" />
        </div>
        <div className="map-main">
          <div className="card map-box" ref={boxRef}>
            <svg ref={svgRef} className="map-svg" viewBox="0 0 975 610" role="img" aria-label="Map of the United States with the businesses" onClick={(e) => { if (e.target === svgRef.current) setSel(null); }} />
            <div className="map-zoom">
              <button type="button" aria-label="Zoom in" onClick={() => { const m = mapRef.current; if (m) m.svg.transition().duration(250).call(m.zoom.scaleBy, 1.8); }}>+</button>
              <button type="button" aria-label="Zoom out" onClick={() => { const m = mapRef.current; if (m) m.svg.transition().duration(250).call(m.zoom.scaleBy, 1 / 1.8); }}>−</button>
              <button type="button" aria-label="Show the whole country" style={{ fontSize: 11 }} onClick={() => { setSt(""); const m = mapRef.current; if (m) m.reset(); }}>US</button>
            </div>
            <div className="map-tip" ref={tipRef} hidden />
            {!ready || !data ? <div className="map-loading dim">Loading the map…</div> : null}
            <div className="map-legend">
              <div>{MAP_INDUSTRIES.map(i => <span key={i}><i className="map-sw" style={{ background: MAP_COLORS[i] }} />{i}</span>)}</div>
              <div><span><i className="map-sym client" />Client</span><span><i className="map-sym deal" />Deal</span><span><i className="map-sym" />Lead</span><span><i className="map-sym approx" />Area code only</span></div>
            </div>
          </div>
          <div className="card map-panel" aria-live="polite">
            {sel ? (
              <>
                <div>
                  <span className="label">{MAP_KINDS[sel.kind]} · {sel.status}</span>
                  <h2 className="map-biz">{sel.biz || "Unnamed business"}</h2>
                  <div className="dim" style={{ fontSize: 13 }}><i className="map-sw" style={{ background: MAP_COLORS[sel.industry] }} />{sel.industry} · {sel.label || sel.state || ""}</div>
                  <div style={{ marginTop: 6 }}><span className={`map-tag${sel.precision === "areacode" ? " approx" : ""}`}>{MAP_PREC[sel.precision] || "Placed"}</span></div>
                </div>
                <div className="map-facts">
                  <div><span className="label">Monthly sales</span><b className="tnum">{sel.revenue ? fmt.moneyShort(sel.revenue) : "—"}</b></div>
                  <div><span className="label">Funded</span><b className="tnum">{sel.funded ? fmt.moneyShort(sel.funded) : "—"}</b></div>
                </div>
                <div className="map-acts">
                  <button type="button" className="btn btn-sm btn-primary" onClick={() => go(MAP_PAGE[sel.kind], sel.id)}>Open {MAP_KINDS[sel.kind].toLowerCase()}</button>
                  <button type="button" className="btn btn-sm" onClick={() => mapRef.current && mapRef.current.zoomTo(sel, 7)}>Zoom here</button>
                  <LunchNearby kind={sel.kind} id={sel.id} geo={sel} button />
                  <button type="button" className="btn btn-sm" onClick={() => setSel(null)}>Close</button>
                </div>
                <div>
                  <span className="label">Within {rad} miles ({near.length})</span>
                  {near.length ? <div className="map-near">{near.map(o => { const fit = (MAP_FITS[sel.industry] || []).includes(o.p.industry), same = o.p.industry === sel.industry && o.p.industry !== "Other"; return (
                    <button type="button" key={o.p.kind + o.p.id} onClick={() => setSel(o.p)}>
                      <i className="map-sw" style={{ background: MAP_COLORS[o.p.industry] }} /><span className="nm">{o.p.biz || "Unnamed"}</span><span className="ds tnum">{o.d < 1 ? "<1" : Math.round(o.d)} mi</span>
                      <span className="why">{MAP_KINDS[o.p.kind]} · {o.p.industry}{fit ? <> · <b className="map-fit">good referral fit</b></> : same ? " · same industry" : ""}{o.p.precision === "areacode" ? " · approximate" : ""}</span>
                    </button>
                  ); })}</div> : <div className="dim" style={{ fontSize: 13 }}>Nobody shown within {rad} miles. Try a wider distance or turn on more layers.</div>}
                </div>
              </>
            ) : (
              <>
                <div><span className="label">On the map</span><h2 className="map-biz">{visible.length} {visible.length === 1 ? "business" : "businesses"}</h2>
                  <div className="dim" style={{ fontSize: 13 }}>Click a business for its details and who's nearby, or a state to zoom to it.</div></div>
                {located ? <div className="dim" style={{ fontSize: 12.5 }}>{located.placed} of {located.total} placed: {located.address} by address on file, {located.statement} by the address on their bank statement, {located.areacode} by phone area code (approximate).</div> : null}
                {may.manage && status ? (
                  <div className="map-admin">
                    <span className="label">Locations</span>
                    {status.running ? (
                      <div style={{ fontSize: 13 }}><b>Reading addresses in the background:</b> {status.statementsToRead + (status.scansToRead || 0)} left. Each new address is placed at the next scheduled run; reopen the Map to see progress.</div>
                    ) : status.statementsToRead ? <>
                      <div style={{ fontSize: 13 }}>{status.statementsToRead} {status.statementsToRead === 1 ? "statement was" : "statements were"} read before addresses were saved{status.scansToRead ? `, and ${status.scansToRead} ${status.scansToRead === 1 ? "business has" : "businesses have"} only scanned statements` : ""}. Reading them finds the business's address (about ${(status.estimate + (status.scanEstimate || 0)).toFixed(2)}).</div>
                      <button type="button" className="btn btn-sm btn-primary" disabled={!!busy} onClick={() => run("addresses")}>{busy === "addresses" ? "Starting…" : `Find addresses on ${status.statementsToRead + (status.scansToRead || 0)} ${status.statementsToRead + (status.scansToRead || 0) === 1 ? "statement" : "statements"}`}</button>
                    </> : status.scansToRead ? <>
                      <div style={{ fontSize: 13 }}>Addresses found on {status.addressesFound} statements. {status.scansToRead} {status.scansToRead === 1 ? "business has" : "businesses have"} only scanned statements: reading one scan each finds the address (about ${status.scanEstimate.toFixed(2)}).</div>
                      <button type="button" className="btn btn-sm btn-primary" disabled={!!busy} onClick={() => run("addresses")}>{busy === "addresses" ? "Starting…" : `Read addresses on ${status.scansToRead} scans`}</button>
                    </> : <div style={{ fontSize: 13 }}>Addresses found on {status.addressesFound} statements.</div>}
                    <button type="button" className="btn btn-sm" disabled={!!busy} onClick={() => run("locate")}>{busy === "locate" ? "Starting…" : "Place everyone now"}</button>
                    <div className="dim" style={{ fontSize: 12 }}>New addresses are placed by the scheduled run every 30 minutes on weekdays; this does it now. Free.</div>
                  </div>
                ) : null}
              </>
            )}
          </div>
        </div>
        <div className="card map-stats">
          <div><span>Businesses</span><b className="tnum">{visible.length}</b></div>
          <div><span>States</span><b className="tnum">{new Set(visible.map(p => p.state).filter(Boolean)).size}</b></div>
          <div><span>Funded</span><b className="tnum">{fmt.moneyShort(funded)}</b></div>
          <div><span>Avg monthly sales</span><b className="tnum">{avgRev ? fmt.moneyShort(avgRev) : "—"}</b></div>
        </div>
        <div className="map-cols">
          <div className="card"><span className="label">By state</span><div className="map-bars">{bar(byState, pickState)}</div></div>
          <div className="card"><span className="label">By industry</span><div className="map-bars">{bar(byInd, (k) => setInd(k), (k) => MAP_COLORS[k])}</div></div>
        </div>
      </div>
    </main>
  );
}

// "Show on map" in a lead's, deal's or client's file.
function ShowOnMap({ kind, id }) {
  return <button type="button" className="link-btn map-link" onClick={() => go("map", `${kind}:${id}`)}><Icon name="pin" size={13} />Show on map</button>;
}

// "Lunch nearby": the 3 closest Italian restaurants to a business placed by its street address (lunch.js; Google
// Places, asked on click). Not offered for one placed by its phone's area code, where "nearby" would be a guess.
const lunchReady = (geo) => !!(geo && (geo.precision === "address" || geo.precision === "statement") && geo.lat != null && !geo.missed);
// A matched Google listing's location, while it may be kept (30 days; places.js).
// …and only for the address it was matched for.
const freshPlace = (g, geo) => !!(g && g.placeId && g.lat != null && g.at && Date.now() - Date.parse(g.at) < 27 * 86400000 && (!geo || g.query === geo.query));
const placeForgotten = new Set();   // "Not this business" on this page, before the file is read again
const lunchSeen = new Map();   // this page's answers, so opening it again doesn't ask Google again
const compSeen = new Map();
const LUNCH_COLOR = "#D9480F", RIVAL_COLOR = "#5F3DC4", RIVAL_LETTERS = ["A", "B", "C"];
function LunchNearby({ kind, id, geo, button }) {
  const [open, setOpen] = useState(false);
  if (!lunchReady(geo)) return null;
  return (
    <>
      {button ? <button type="button" className="btn btn-sm" onClick={() => setOpen(true)}><Icon name="lunch" size={14} />Lunch nearby</button>
        : <button type="button" className="link-btn map-link" onClick={() => setOpen(true)}><Icon name="lunch" size={13} />Lunch nearby</button>}
      {open ? <LunchSheet kind={kind} id={id} onClose={() => setOpen(false)} /> : null}
    </>
  );
}
function LunchSheet({ kind, id, onClose }) {
  const key = `${kind}:${id}`;
  const [res, setRes] = useState(lunchSeen.get(key) || null);
  const [err, setErr] = useState("");
  useEffect(() => {
    if (res) return;
    let live = true;
    API.lunchNearby(kind, id).then(r => { lunchSeen.set(key, r); if (live) setRes(r); }).catch(e => { if (live) setErr(e.message || "Couldn't look this up."); });
    return () => { live = false; };
  }, []);
  const places = (res && res.places) || [];
  return (
    <Portal><Sheet eyebrow="Lunch nearby" title="Italian restaurants closest to the business" onClose={onClose}>
      <div className="form-section">
        {err ? <div className="banner" role="alert">{err}</div>
          : !res ? <div className="dim">Looking…</div>
          : !places.length ? <div className="dim">No Italian restaurants found within 10 miles{res.near ? ` of ${res.near}` : ""}.</div>
          : <LunchRows places={places} />}
        {places.length ? <div className="lunch-src dim">Results from Google Maps</div> : null}
      </div>
    </Sheet></Portal>
  );
}

function LunchRows({ places, numbered }) {
  return (
    <div className="lunch-list">{places.map((p, i) => (
      <div className="lunch-row" key={i}>
        <div className="lunch-top"><b>{numbered ? <span className="lunch-n" aria-hidden="true">{i + 1}</span> : null}{p.name}</b>{p.miles != null ? <span className="tnum dim">{p.miles < 0.1 ? "<0.1" : p.miles} mi</span> : null}</div>
        <div className="dim">{p.address}</div>
        <div className="lunch-meta">
          {p.rating != null ? <span className="tnum">★ {p.rating.toFixed(1)}{p.ratings ? <span className="dim"> ({p.ratings.toLocaleString("en-US")})</span> : null}</span> : null}
          {p.phone ? <a href={`tel:${p.phone.replace(/[^\d+]/g, "")}`}><Icon name="phone" size={13} />{p.phone}</a> : null}
          {p.mapsUrl ? <a href={p.mapsUrl} target="_blank" rel="noopener noreferrer">Google Maps</a> : null}
        </div>
      </div>
    ))}</div>
  );
}

// ── The Map tab in a lead's, deal's or client's file ─────────────────────────
// Where the merchant is, and (when it's placed by a street address) the 3 closest Italian restaurants, on Google's
// interactive map (Maps JavaScript API). The browser key comes from /api/map/config and is locked to byltcapitalsolutions.com in
// Google Cloud, with a daily cap on map loads. Loaded once, on the first Map tab opened.
let gmapsLoading = null, gmapsAuthFailed = false;
function loadGoogleMaps() {
  if (gmapsLoading) return gmapsLoading;
  gmapsLoading = API.mapConfig().then(cfg => {
    if (!cfg || !cfg.browserKey) throw new Error("nokey");
    return new Promise((resolve, reject) => {
      window.__efMapsReady = () => Promise.all([google.maps.importLibrary("maps"), google.maps.importLibrary("marker")])
        .then(([maps, marker]) => resolve({ Map: maps.Map, Marker: marker.Marker, LatLngBounds: google.maps.LatLngBounds, SymbolPath: google.maps.SymbolPath }), reject);
      // Google calls this when it refuses the key (wrong site, API not on, over the daily cap).
      window.gm_authFailure = () => { gmapsAuthFailed = true; window.dispatchEvent(new Event("ef-maps-refused")); };
      const tag = document.createElement("script");
      tag.src = `https://maps.googleapis.com/maps/api/js?key=${encodeURIComponent(cfg.browserKey)}&v=weekly&loading=async&callback=__efMapsReady`;
      tag.async = true;
      tag.onerror = () => { tag.remove(); reject(new Error("load")); };
      document.head.appendChild(tag);
    });
  });
  gmapsLoading.catch(() => { gmapsLoading = null; });
  return gmapsLoading;
}

function FileMapTab({ kind, id, geo, biz, gplace }) {
  const exact = lunchReady(geo);
  const key = `${kind}:${id}`;
  const canCompete = kind === "lead" || kind === "client";
  // Lunch spots and competitors are looked up only on a click (each one asks Google), then kept for this page.
  const [lunch, setLunch] = useState(lunchSeen.get(key) || null);
  const [comp, setComp] = useState(compSeen.get(key) || null);
  const [busy, setBusy] = useState("");
  const [err, setErr] = useState({});
  // The business's Google listing: saved on the file once matched (places.js), or just found by a lookup.
  const [place, setPlace] = useState(() => (gplace && gplace.placeId && !placeForgotten.has(`${key}:${gplace.placeId}`) && (!geo || gplace.query === geo.query)
    ? { placeId: gplace.placeId, how: gplace.how, lat: freshPlace(gplace, geo) ? gplace.lat : null, lng: freshPlace(gplace, geo) ? gplace.lng : null } : null));
  const [placeMsg, setPlaceMsg] = useState("");
  // A later answer without the listing's name or address (read from the saved place) keeps the ones already shown.
  const latest = (r) => { if (r && r.listing) setPlace(p => { const same = p && p.placeId === r.listing.placeId; const keep = (k) => r.listing[k] || (same ? p[k] : null);
    return { ...r.listing, name: keep("name"), address: keep("address"), mapsUrl: keep("mapsUrl"), lat: r.from ? r.from.lat : p && p.lat, lng: r.from ? r.from.lng : p && p.lng }; }); else setPlace(null); };
  const find = async (what) => {
    setBusy(what); setErr(e => ({ ...e, [what]: "" }));
    try {
      const r = what === "lunch" ? await API.lunchNearby(kind, id) : await API.competitors(kind, id);
      (what === "lunch" ? lunchSeen : compSeen).set(key, r); what === "lunch" ? setLunch(r) : setComp(r); latest(r);
    } catch (e) { setErr(x => ({ ...x, [what]: e.message || "Couldn't look this up." })); }
    setBusy("");
  };
  const notThis = async () => {
    try { await API.rejectPlace(kind, id); if (place && place.placeId) placeForgotten.add(`${key}:${place.placeId}`); setPlace(null); setLunch(null); setComp(null); lunchSeen.delete(key); compSeen.delete(key); setPlaceMsg("Forgotten. The next lookup searches Google again, without that listing."); }
    catch (e) { setPlaceMsg(e.message || "Couldn't change that."); }
  };
  const home = place && place.lat != null ? { lat: place.lat, lng: place.lng } : geo ? { lat: geo.lat, lng: geo.lng } : null;

  // The map (one map load per tab opened): the business's pin; numbered lunch pins and lettered competitor pins.
  const boxRef = useRef(null), gRef = useRef(null), mapObj = useRef(null), homePin = useRef(null);
  const [mapReady, setMapReady] = useState(false);
  const [mapErr, setMapErr] = useState(gmapsAuthFailed ? "refused" : "");
  useEffect(() => {
    if (!exact || !home) return;
    let live = true;
    const refused = () => live && setMapErr("refused");
    window.addEventListener("ef-maps-refused", refused);
    loadGoogleMaps().then(g => {
      if (!live || !boxRef.current) return;
      const map = new g.Map(boxRef.current, { center: home, zoom: 15, mapTypeControl: false, streetViewControl: false, fullscreenControl: true, clickableIcons: false });
      homePin.current = new g.Marker({ map, position: home, title: biz || "The business", zIndex: 10 });
      gRef.current = g; mapObj.current = map; setMapReady(true);
    }).catch(e => live && setMapErr(e && e.message === "nokey" ? "nokey" : "load"));
    return () => { live = false; window.removeEventListener("ef-maps-refused", refused); };
  }, [key, exact]);
  useEffect(() => {
    const g = gRef.current, map = mapObj.current;
    if (!mapReady || !g || !map || !home) return;
    if (homePin.current && homePin.current.setPosition) homePin.current.setPosition(home);
    const places = ((lunch && lunch.places) || []).filter(p => p.lat != null);
    const rivals = ((comp && comp.competitors) || []).filter(p => p.lat != null);
    if (!places.length && !rivals.length) { map.setCenter(home); return; }
    const bounds = new g.LatLngBounds(); bounds.extend(home);
    const pin = (p, text, color) => { bounds.extend({ lat: p.lat, lng: p.lng });
      return new g.Marker({ map, position: { lat: p.lat, lng: p.lng }, title: p.name, label: { text, color: "#fff", fontSize: "12px", fontWeight: "700" },
        icon: { path: g.SymbolPath.CIRCLE, scale: 11, fillColor: color, fillOpacity: 1, strokeColor: "#fff", strokeWeight: 2 } }); };
    const pins = [...places.map((p, i) => pin(p, String(i + 1), LUNCH_COLOR)), ...rivals.map((p, i) => pin(p, RIVAL_LETTERS[i], RIVAL_COLOR))];
    map.fitBounds(bounds, 48);
    return () => pins.forEach(m => m.setMap(null));
  }, [mapReady, lunch, comp, home && home.lat, home && home.lng]);

  if (!geo || geo.lat == null) return (
    <div className="card card-body">
      <span className="label">Location</span>
      <div style={{ marginTop: 6 }}>Not placed yet. A business is placed from its address on file, the address on its bank statements, or its phone's area code; new ones are placed every 30 minutes on weekdays.</div>
    </div>
  );
  const places = (lunch && lunch.places) || [];
  const findBtn = (what, label) => <button type="button" className="btn btn-sm" disabled={!!busy} onClick={() => find(what)}>{busy === what ? "Looking…" : label}</button>;
  return (
    <div className="file-map">
      <div className="card card-body file-map-head">
        <div>
          <span className="label">Location</span>
          <div className="file-map-where"><b>{geo.label || geo.state || "Placed"}</b><span className={`map-tag${exact ? "" : " approx"}`}>{MAP_PREC[geo.precision] || "Placed"}</span></div>
          {exact && geo.query && !/^areacode:/.test(geo.query) ? <div className="file-map-street">{geo.query}</div> : null}
          {exact && place && place.placeId ? (
            <div className="file-map-listing">
              <span>Shop: {place.name ? <b>{place.name}</b> : "its Google listing"}{place.address ? ` · ${place.address}` : ""}</span>
              {place.mapsUrl ? <a href={place.mapsUrl} target="_blank" rel="noopener noreferrer">Google Maps</a> : null}
              <button type="button" className="link-btn" onClick={notThis}>Not this business</button>
            </div>
          ) : null}
          {placeMsg ? <div className="dim" style={{ fontSize: 12.5, marginTop: 4 }}>{placeMsg}</div> : null}
        </div>
        <ShowOnMap kind={kind} id={id} />
      </div>
      {!exact ? (
        <div className="card card-body dim">This business is placed only by its phone's area code, so it's somewhere around {geo.label ? geo.label.replace(/ area$/, "") : "that city"}. Restaurants and competitors nearby need a street address: add the business address in its details, or upload a bank statement with the address on it.</div>
      ) : (
        <div className="file-map-grid">
          <div className="card file-map-box">
            {mapErr ? <div className="file-map-msg dim">{mapErr === "nokey" ? "The map isn't set up yet (the Google map key hasn't been added)." : mapErr === "refused" ? "Google didn't accept the map key (check it's locked to byltcapitalsolutions.com with Maps JavaScript API on, or today's map limit was reached)." : "The map couldn't load. Check the connection and open the tab again."}</div>
              : <div ref={boxRef} className="file-map-canvas" role="region" aria-label={`Map of ${biz || "the business"}`} />}
          </div>
          <div className="card card-body">
            <div className="file-map-rivals-head"><span className="label">Lunch nearby · Italian</span>{findBtn("lunch", lunch ? "Look again" : "Find lunch spots")}</div>
            <div style={{ marginTop: 8 }}>
              {err.lunch ? <div className="banner" role="alert">{err.lunch}</div>
                : !lunch ? <div className="dim" style={{ fontSize: 13 }}>The 3 closest Italian restaurants, to send lunch from.</div>
                : !places.length ? <div className="dim">No Italian restaurants found within 10 miles.</div>
                : <LunchRows places={places} numbered />}
              {places.length ? <div className="lunch-src dim">Results from Google Maps</div> : null}
            </div>
          </div>
        </div>
      )}
      {exact && canCompete ? <div className="card card-body file-map-rivals">
        <div className="file-map-rivals-head"><span className="label">Competitors</span>{findBtn("comp", comp ? "Look again" : "Find competitors")}</div>
        {err.comp ? <div className="banner" role="alert" style={{ marginTop: 8 }}>{err.comp}</div>
          : !comp ? <div className="dim" style={{ fontSize: 13, marginTop: 6 }}>The 3 closest businesses of the same type. Independents only, unless this business is a chain itself.</div>
          : comp.status === "nolisting" ? <div className="dim" style={{ marginTop: 6 }}>No listing found. Google has no listing for this business near its address, so there's nothing to compare it with. If the address is wrong, fix it in Details.</div>
          : comp.status === "notype" ? <div className="dim" style={{ marginTop: 6 }}>Its Google listing has no business type to compare by.</div>
          : <>
              <div className="dim" style={{ fontSize: 13, marginTop: 6 }}>{comp.businessType || "Same type"} · within {comp.miles} miles</div>
              {comp.competitors.length ? <div className="lunch-list file-map-rival-list" style={{ marginTop: 8 }}>{comp.competitors.map((p, i) => (
                <div className="lunch-row" key={i}>
                  <div className="lunch-top"><b><span className="lunch-n rival" aria-hidden="true">{RIVAL_LETTERS[i]}</span>{p.name}</b>{p.miles != null ? <span className="tnum dim">{p.miles < 0.1 ? "<0.1" : p.miles} mi</span> : null}</div>
                  <div className="dim">{[p.type, p.chain ? "Chain" : "Independent"].filter(Boolean).join(" · ")}</div>
                  <div className="lunch-meta"><span className="dim">{p.address}</span>{p.mapsUrl ? <a href={p.mapsUrl} target="_blank" rel="noopener noreferrer">Google Maps</a> : null}</div>
                </div>
              ))}</div> : <div className="dim" style={{ marginTop: 6 }}>No independent {String(comp.businessType || "businesses of this type").toLowerCase()} found within 25 miles.</div>}
              <div className="lunch-src dim">Results from Google Maps</div>
            </>}
      </div> : null}
    </div>
  );
}

Object.assign(window, { MapView, ShowOnMap, LunchNearby, FileMapTab });
