/* ============================================================
   Prospecting — app shell, routing, assessment flow, tweaks
   The backend (FastAPI on :8766) holds the authoritative site list
   and runs every assessor. This file just orchestrates UI state.
   ============================================================ */

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "density": "comfortable",
  "showConfidence": true,
  "clickThrough": true
}/*EDITMODE-END*/;

/* Grid overlay radius for the "show grid on map" toggle (metres). */
const PC_GRID_OVERLAY_RADIUS_M = 5000;

const Rail = ({ view, onView, onNew }) => (
  <aside className="pc-rail">
    <a className="pc-rail-logo" href="#" onClick={e => e.preventDefault()}>
      <img src="assets/logos/Lodestone_Icon_yellow.svg" alt="Lodestone" />
    </a>
    <nav className="pc-rail-nav">
      <button className={`pc-rail-btn ${view === 'workspace' ? 'is-active' : ''}`} onClick={() => onView('workspace')}>
        {PCI.map}<span className="pc-rail-lbl">Map</span>
      </button>
      <button className={`pc-rail-btn ${view === 'pipeline' ? 'is-active' : ''}`} onClick={() => onView('pipeline')}>
        {PCI.list}<span className="pc-rail-lbl">Pipeline</span>
      </button>
    </nav>
    <div className="pc-rail-foot">
      <div className="pc-rail-avatar" title="Prospecting team">PT</div>
    </div>
  </aside>
);

/* Toast is defined in ReportPanel.jsx so MapWorkspace can use it too
   (Babel script order is icons → ReportPanel → … → MapWorkspace → App). */

function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const [view, setView] = React.useState('workspace');
  const [sites, setSites] = React.useState([]);              // backend-fetched list
  const [activeSite, setActiveSite] = React.useState(null);
  const [pinLocation, setPinLocation] = React.useState(null); // [lng, lat] | null
  const [gridOverlayOn, setGridOverlayOn] = React.useState(false);
  const [gridOverlay, setGridOverlay] = React.useState(null); // {lines, substations}
  const [panelState, setPanelState] = React.useState('empty'); // empty|loading|report|error
  const [loadingStep, setLoadingStep] = React.useState(0);
  const [loadingError, setLoadingError] = React.useState(null);
  const [drawer, setDrawer] = React.useState({ open: false, assessor: null });
  const [toast, setToast] = React.useState(null);
  const [panelW, setPanelW] = React.useState(() =>
    Math.round(Math.min(Math.max((window.innerWidth || 1280) * 0.3, 420), 620)));
  const timers = React.useRef([]);

  // Focal point used for the grid overlay query AND the always-on pin marker.
  // Active site wins over a dropped pin.
  const focalPoint = activeSite?.centroid || pinLocation || null;
  const focalLabel = activeSite?.name || (pinLocation ? 'Dropped pin' : null);

  const clearTimers = () => { timers.current.forEach(clearTimeout); timers.current = []; };

  // Initial site list from the backend, with a sticky banner if it's down so
  // the user notices before they try to draw/save/edit.
  React.useEffect(() => {
    let cancelled = false;
    PCApi.listSites()
      .then(list => { if (!cancelled) setSites(list); })
      .catch(err => {
        console.warn('Could not reach backend at', PCApi.BASE, err);
        setToast({
          kind: 'error',
          msg: 'Backend not reachable. Run Install-Prospecting.ps1 once, or restart the ProspectingBackend scheduled task.',
          actions: [{ label: 'Retry', primary: true, onClick: () => window.location.reload() }],
        });
        // no auto-dismiss — they need to act on it
      });
    return () => { cancelled = true; };
  }, []);

  // run the backend assessment with a brief loading-cascade animation overlaid
  const runAssessment = async ({ geometry, name, address }) => {
    clearTimers();
    setActiveSite(null);
    setLoadingError(null);
    setPanelState('loading');
    setLoadingStep(0);

    const cascadeStepMs = 360;
    const steps = window.PC_LOADING_STEPS.length;
    for (let i = 1; i <= steps; i++) {
      timers.current.push(setTimeout(() => setLoadingStep(i), i * cascadeStepMs));
    }
    const minWaitPromise = new Promise(r => setTimeout(r, steps * cascadeStepMs + 240));

    try {
      const [site] = await Promise.all([
        PCApi.assessNow({ geometry, name, address }),
        minWaitPromise,
      ]);
      // Cache the freshly assessed full record so re-opening it skips the fetch.
      fullSiteCache.current.set(site.id, site);
      // Replace its summary in the list (or insert at the front for new IDs).
      const summary = {
        id: site.id, name: site.name, address: site.address, region: site.region,
        area_ha: site.area_ha, shape_descriptor: site.shape_descriptor,
        overall_score: site.overall_score, overall_status: site.overall_status,
        assessed_at: site.assessed_at, centroid: site.centroid,
      };
      setSites(prev => [summary, ...prev.filter(s => s.id !== site.id)]);
      setActiveSite(site);
      setPanelState('report');
    } catch (err) {
      console.error('Assessment failed:', err);
      setLoadingError(err.message || String(err));
      setPanelState('error');
    }
  };

  const onDrawComplete = (info) => {
    runAssessment({
      geometry: info.geometry,
      name: null,
      address: `Drawn boundary · ${Math.abs(info.centroid[1]).toFixed(3)}°S, ${Math.abs(info.centroid[0]).toFixed(3)}°E`,
    });
  };

  // Bumped by Discard to force MapWorkspace to re-show the active site's
  // *stored* geometry, throwing away the edits the user dragged in.
  const [revertNonce, setRevertNonce] = React.useState(0);
  // Latest edited geometry while the user is mid-edit. Updated on every
  // vertex-drag release so Save commits the most recent shape.
  const pendingEditGeom = React.useRef(null);

  const saveActiveEdit = async () => {
    const geometry = pendingEditGeom.current;
    if (!activeSite || !geometry) return;
    const siteId = activeSite.id;
    setToast({ msg: 'Saving boundary and re-running assessors…' });
    try {
      const updated = await PCApi.updateSiteGeometry(siteId, geometry);
      fullSiteCache.current.set(updated.id, updated);
      setActiveSite(updated);
      const summary = {
        id: updated.id, name: updated.name, address: updated.address, region: updated.region,
        area_ha: updated.area_ha, shape_descriptor: updated.shape_descriptor,
        overall_score: updated.overall_score, overall_status: updated.overall_status,
        assessed_at: updated.assessed_at, centroid: updated.centroid,
      };
      setSites(prev => prev.map(s => s.id === updated.id ? summary : s));
      pendingEditGeom.current = null;
      setToast({ msg: 'Boundary updated.', kind: 'ok' });
      setTimeout(() => setToast(null), 2400);
    } catch (err) {
      console.error('save failed', err);
      // Distinguish 'backend unreachable' (the common case if uvicorn is
      // down) from a real server-side error so the user knows what to fix.
      const isNetworkFailure = err && (err.message === 'Failed to fetch' ||
        /NetworkError|TypeError: Load failed/.test(String(err.message || err)));
      const msg = isNetworkFailure
        ? 'Save failed — backend not reachable. Is Start-Prospecting.ps1 running?'
        : `Save failed: ${err.message || err}`;
      setToast({
        msg, kind: 'error',
        actions: [
          { label: 'Retry', primary: true, onClick: saveActiveEdit },
          { label: 'Dismiss', onClick: () => setToast(null) },
        ],
      });
    }
  };

  const discardActiveEdit = () => {
    pendingEditGeom.current = null;
    setToast(null);
    setRevertNonce(n => n + 1);  // re-show stored geometry on the map
  };

  // Fires after every vertex-drag release while in edit mode. Stash the new
  // geometry and show the Save / Discard toast (replaces the edit hint).
  const onEditChange = ({ geometry }) => {
    if (!activeSite) return;
    pendingEditGeom.current = geometry;
    setToast({
      msg: 'Boundary edited',
      actions: [
        { label: 'Discard', onClick: discardActiveEdit },
        { label: 'Save', primary: true, onClick: saveActiveEdit },
      ],
    });
  };

  // Legacy onEditComplete (fires when leaving edit mode). With onEditChange
  // already showing the toast on first drag, this is a no-op — kept so the
  // mapController's existing call site doesn't need to be removed.
  const onEditComplete = () => {};
  // Pin drop is a non-destructive marker. Doesn't create a site — the user
  // chooses to draw a boundary around it (or run a grid lookup via the toggle).
  const onPinDrop = (point, label) => {
    setPinLocation(point);
    // pin is the new focal point; if grid overlay is on, App effect will refetch
  };

  // Fetch the nearby grid whenever the focal geometry or toggle changes.
  // We send the site polygon when a site is active (so buffer rings hug the
  // boundary shape); otherwise the dropped pin's Point (concentric circles).
  const focalGeometry = activeSite?.geometry || (pinLocation ? { type: 'Point', coordinates: pinLocation } : null);
  React.useEffect(() => {
    if (!gridOverlayOn || !focalGeometry) { setGridOverlay(null); return; }
    let cancelled = false;
    PCApi.gridNearby({
      geometry: focalGeometry,
      search_radius_m: PC_GRID_OVERLAY_RADIUS_M,
      buffer_radii_m: [1000, 2500, 5000],
    })
      .then(r => { if (!cancelled) setGridOverlay({ lines: r.lines, substations: r.substations, buffers: r.buffers }); })
      .catch(err => {
        console.warn('grid-nearby fetch failed', err);
        setToast('Could not load nearby grid infrastructure.');
        setTimeout(() => setToast(null), 3200);
      });
    return () => { cancelled = true; };
  }, [gridOverlayOn, JSON.stringify(focalGeometry)]);

  // Cache of fully-loaded site records by id, so re-opening a site doesn't
  // re-fetch its geometry + assessor results.
  const fullSiteCache = React.useRef(new Map());

  const openSite = async (id) => {
    if (id == null) { setActiveSite(null); setPanelState('empty'); setView('workspace'); return; }
    clearTimers();
    setView('workspace');
    // If we already have the full record cached, render immediately.
    const cached = fullSiteCache.current.get(id);
    if (cached) { setActiveSite(cached); setPanelState('report'); return; }
    // Otherwise show the loading panel and fetch the full record.
    setActiveSite(null);
    setPanelState('loading');
    setLoadingStep(window.PC_LOADING_STEPS.length);  // mid-cascade so it doesn't feel sluggish
    try {
      const full = await PCApi.getSite(id);
      fullSiteCache.current.set(id, full);
      setActiveSite(full);
      setPanelState('report');
    } catch (err) {
      console.error('failed to load site', id, err);
      setLoadingError(err.message || String(err));
      setPanelState('error');
    }
  };

  const onCloseReport = () => { clearTimers(); setActiveSite(null); setPanelState('empty'); };
  const onExport = () => {
    setToast('Export queued — the PDF report builder is wired to a placeholder for now.');
    setTimeout(() => setToast(null), 3200);
  };
  const openAssessor = (a) => setDrawer({ open: true, assessor: a });
  const closeDrawer = () => setDrawer(d => ({ ...d, open: false }));

  React.useEffect(() => () => clearTimers(), []);

  const cls = `pc-app ${t.density === 'compact' ? 'compact' : ''} ${t.showConfidence ? '' : 'no-conf'}`;

  return (
    <div className={cls}>
      <Rail view={view} onView={(v) => setView(v)} />
      <main className="pc-main">
        {/* Workspace stays mounted so the map keeps its state across nav */}
        <div style={{ position: 'absolute', inset: 0, visibility: view === 'workspace' ? 'visible' : 'hidden' }}>
          <MapWorkspace
            toast={toast}
            sites={sites}
            activeSite={activeSite}
            panelState={panelState}
            loadingStep={loadingStep}
            loadingError={loadingError}
            clickThrough={t.clickThrough}
            panelW={panelW}
            setPanelW={setPanelW}
            gridOverlayOn={gridOverlayOn && !!focalPoint}
            gridOverlay={gridOverlay}
            focalPoint={focalPoint}
            focalLabel={focalLabel}
            revertNonce={revertNonce}
            onDrawComplete={onDrawComplete}
            onEditComplete={onEditComplete}
            onEditChange={onEditChange}
            onPinDrop={onPinDrop}
            onSiteClickMap={openSite}
            onCloseReport={onCloseReport}
            onExport={onExport}
            onOpenAssessor={openAssessor}
            gridSectionToggle={{
              on: gridOverlayOn,
              canEnable: !!focalPoint,
              onChange: setGridOverlayOn,
            }}
          />
        </div>
        {view === 'pipeline' && <Pipeline sites={sites} onOpen={openSite} />}

        <DetailDrawer assessor={drawer.assessor} open={drawer.open} onClose={closeDrawer} width={panelW} />
        {/* Toasts in workspace view are rendered inside MapWorkspace so they
            centre on the map container (not the whole pc-main). Pipeline view
            still uses the top-level toast so it sits over the table. */}
        {view !== 'workspace' && <Toast msg={toast} />}
      </main>

      <TweaksPanel>
        <TweakSection label="Report density" />
        <TweakRadio label="Card density" value={t.density} options={['comfortable', 'compact']}
                    onChange={v => setTweak('density', v)} />
        <TweakSection label="Detail" />
        <TweakToggle label="Show confidence" value={t.showConfidence} onChange={v => setTweak('showConfidence', v)} />
        <TweakToggle label="Click card for full detail" value={t.clickThrough} onChange={v => setTweak('clickThrough', v)} />
      </TweaksPanel>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
