/* global React, MiniDayDial, SH_EL, AndroidStatus */
// Skyhearth mobile — EXPEDITION as a Slay-the-Spire route map, wired to the
// SHARED CORE. The ROUTE (which node, what's walked, the run's haul) is local
// run-state; every CONDITION change (Hull, appetite, fatigue, bond) and the
// final deposit go through the reducer — so the field mutates the SAME spirit
// the Port raised, and checks read the SAME trained values.

const { useState: useSm, useRef: useRm, useEffect: useEm } = React;
const FC = window.SkyhearthCore;

// ---- the route (Zone-0 identities, laid out as a clean StS lattice) ----
const NODES = {
  launch: { type: 'vessel',   x: 178, y: 902, name: 'Cast Off',     blurb: 'The vessel slips the Skyhearth moorings into the Whispering Strata.' },
  a:      { type: 'forage',   x: 70,  y: 772, name: 'Kelp Drift',   blurb: 'A pale drift of kelp clouds the strata — workable on Nature.' },
  d:      { type: 'gate',     x: 178, y: 772, name: 'Tide-Gate',    blurb: 'A standing wall of tide. It yields only to a steady Earth.' },
  b:      { type: 'danger',   x: 286, y: 772, name: 'Reef Squall',  blurb: 'Charged spray claws the reef. A quick way down — if you brave it.', force: 'water', band: [4, 20], hullFail: 3 },
  c:      { type: 'forage',   x: 116, y: 642, name: 'Bloom Shelf',  blurb: 'A shelf thick with spore-bloom. The larder could grow here.' },
  h:      { type: 'recruit',  x: 240, y: 642, name: 'Navigator',    blurb: 'A navigator hails from the lee, charts in hand.', recruitKey: 'nav' },
  f:      { type: 'forage',   x: 116, y: 512, name: 'Spore Vault',  blurb: 'A pocket of still air holds a vault of dried spores.' },
  g:      { type: 'forage',   x: 240, y: 512, name: 'Glimmer Pool', blurb: 'A pool of glimmer-water. Sweet, if the Nature reads true.' },
  i:      { type: 'forage',   x: 116, y: 382, name: 'Mistflats',    blurb: 'Wide mistflats. Thin pickings, but safe enough.' },
  k:      { type: 'danger',   x: 240, y: 382, name: 'The Squall',   blurb: 'A wall of charged air guards the gateway tail.', force: 'lightning', band: [16, 36], hullFail: 6 },
  j:      { type: 'treasure', x: 116, y: 252, name: 'Rich Cache',   blurb: 'A corner the tides forgot — trade-goods and an offering wait.' },
  e1:     { type: 'recruit',  x: 240, y: 252, name: 'Supply Lead',  blurb: 'A quartermaster willing to crew for the Hearth.', recruitKey: 'supply' },
  end3:   { type: 'gateway',  x: 178, y: 118, name: 'The Gateway',  blurb: 'The keyed gateway. Open it and the deeper archipelago answers.' },
};
const EDGES = [
  ['launch', 'a'], ['launch', 'd'], ['launch', 'b'],
  ['a', 'c'], ['d', 'c'], ['d', 'h'], ['b', 'h'],
  ['c', 'f'], ['c', 'g'], ['h', 'g'],
  ['f', 'i'], ['g', 'i'], ['g', 'k'],
  ['i', 'j'], ['k', 'j'], ['k', 'e1'], ['i', 'e1'],
  ['j', 'end3'], ['e1', 'end3'],
];
const MAP_W = 356, MAP_H = 980;

const NTYPE = {
  vessel:   { glyph: '⚓', color: 'var(--ink)',  label: 'Launch' },
  forage:   { glyph: '❦', color: '#5d7a3f',      label: 'Forage' },
  gate:     { glyph: '⌖', color: '#6b4a2b',      label: 'Gate' },
  danger:   { glyph: '✶', color: '#a12b22',      label: 'Hazard' },
  recruit:  { glyph: '✦', color: '#b08c3c',      label: 'Recruit', filled: true },
  treasure: { glyph: '◈', color: '#b8632a',      label: 'Cache' },
  gateway:  { glyph: '◉', color: 'var(--ink)',   label: 'Gateway', filled: true, boss: true },
};

function outTargets(id) { return EDGES.filter(([a]) => a === id).map(([, b]) => b); }

function FieldRoute({ state, dispatch, onReturnScreen, tabBar }) {
  const viewRef = useRm(null);
  const [run, setRun] = useSm({ current: 'launch', visited: ['launch'], haul: { staple: 0, coins: 0, offerings: {} }, recruits: [], complete: false, gatewayOpened: false });
  const [enc, setEnc] = useSm(null);     // { nodeId, phase:'choose'|'result', text, tone }
  const [husk, setHusk] = useSm(false);
  const { current, visited, complete } = run;
  const reachable = complete || husk ? [] : outTargets(current).filter(t => !visited.includes(t));
  const hull = state.hull;
  const elv = (id) => state.elements[id] ? (state.elements[id].mastery || 0) : 0;

  // clock-out mid-run → the husk flees home with the haul (manifestation §end-of-run)
  useEm(() => { if (state.manifestDays <= 0 && !husk && !complete) { setHusk(true); setEnc(null); } }, [state.manifestDays]);

  // --- momentum drag (StS-style vertical haul) ---
  const [offset, setOffset] = useSm(-346);
  const [grabbing, setGrabbing] = useSm(false);
  const offRef = useRm(-346);
  useEm(() => { offRef.current = offset; }, [offset]);
  const drag = useRm({ active: false, moved: false, startY: 0, startOff: 0, lastY: 0, lastT: 0, vel: 0, raf: 0, nodeId: null });
  function clampOff(o) { const v = viewRef.current; const vh = v ? v.clientHeight : 634; return Math.max(Math.min(0, vh - MAP_H), Math.min(0, o)); }
  useEm(() => {
    const v = viewRef.current; if (!v) return; const n = NODES[current]; if (!n) return;
    const target = clampOff(-(n.y - v.clientHeight * 0.58));
    cancelAnimationFrame(drag.current.raf);
    const animate = () => { const cur = offRef.current, diff = target - cur; if (Math.abs(diff) < 0.5) { setOffset(target); return; } setOffset(cur + diff * 0.16); drag.current.raf = requestAnimationFrame(animate); };
    animate(); return () => cancelAnimationFrame(drag.current.raf);
  }, [current]);
  useEm(() => () => cancelAnimationFrame(drag.current.raf), []);
  function onPanDown(e) { cancelAnimationFrame(drag.current.raf); const nodeEl = e.target.closest && e.target.closest('[data-node]'); drag.current = { active: true, moved: false, startY: e.clientY, startOff: offRef.current, lastY: e.clientY, lastT: performance.now(), vel: 0, raf: 0, nodeId: nodeEl ? nodeEl.getAttribute('data-node') : null }; try { e.currentTarget.setPointerCapture(e.pointerId); } catch (_) {} }
  function onPanMove(e) { const d = drag.current; if (!d.active) return; const dy = e.clientY - d.startY; if (Math.abs(dy) > 6 && !d.moved) { d.moved = true; setGrabbing(true); } const now = performance.now(); d.vel = (e.clientY - d.lastY) / Math.max(1, now - d.lastT); d.lastY = e.clientY; d.lastT = now; setOffset(clampOff(d.startOff + dy)); }
  function onPanUp() { const d = drag.current; if (!d.active) return; d.active = false; setGrabbing(false); if (!d.moved) { if (d.nodeId && reachable.includes(d.nodeId)) travelTo(d.nodeId); return; } let vel = d.vel * 16; if (performance.now() - d.lastT > 80) vel = 0; let cur = offRef.current; const step = () => { vel *= 0.92; cur += vel; const c = clampOff(cur); setOffset(c); if (c === cur && Math.abs(vel) > 0.4) drag.current.raf = requestAnimationFrame(step); }; if (Math.abs(vel) > 0.5) drag.current.raf = requestAnimationFrame(step); }

  function travelTo(id) {
    if (!reachable.includes(id)) return;
    setRun(r => ({ ...r, current: id, visited: [...r.visited, id] }));
    setEnc({ nodeId: id, phase: 'choose' });
  }

  // resolve an encounter choice — spends ONE slot (FIELD_CARE tick = appetite+body),
  // layering the action's Hull / haul / recruit outcome.
  function resolve(id, kind, opt) {
    const node = NODES[id];
    let text = '', tone = 'ok';
    let care = { tick: true };              // every node action spends a slot
    setRun(r => {
      const nr = { ...r, haul: { ...r.haul, offerings: { ...r.haul.offerings } }, recruits: [...r.recruits] };
      if (kind === 'forage') {
        const chance = Math.max(0.2, FC.checkChance(elv('nature'), 2, 10));
        if (Math.random() < chance) { nr.haul.staple += 4; care.fill = 6; care.forageDraw = 'whispering-strata:' + id; text = 'The bloom gives — +4 staple to the larder.'; tone = 'ok'; }
        else { care.hull = -2; text = 'The draw comes up empty — the harness scrapes (−2 Hull).'; tone = 'warn'; }
      } else if (kind === 'skip') { text = 'You leave the perch be and press on.'; tone = 'faded'; }
      else if (kind === 'gate') {
        if (opt === 'pass') { text = `Earth ${elv('earth')} holds the tide — you slip through clean.`; tone = 'ok'; }
        else { care.hull = -4; text = 'You force the gate against the tide (−4 Hull).'; tone = 'warn'; }
      } else if (kind === 'force') {
        const chance = FC.checkChance(elv(node.force), node.band[0], node.band[1]);
        if (Math.random() < chance) { text = `${SH_EL[node.force].name} ${elv(node.force)} carries you through.`; tone = 'ok'; }
        else { care.hull = -node.hullFail; text = `The squall throws you back (−${node.hullFail} Hull).`; tone = 'warn'; }
      } else if (kind === 'finesse') { care.hull = -3; text = 'You thread the edge — sure, but it costs the harness (−3 Hull).'; tone = 'warn'; }
      else if (kind === 'wait') { care.hull = -node.hullFail; text = `You wait it down — slow and battering (−${node.hullFail} Hull).`; tone = 'warn'; }
      else if (kind === 'recruit') { nr.recruits.push(node.recruitKey); text = `${node.name} joins the Hearth ✦`; tone = 'gold'; }
      else if (kind === 'claim') { nr.haul.coins += 6; nr.haul.offerings.nature = (nr.haul.offerings.nature || 0) + 1; text = '+6 coin · +1 offering banked.'; tone = 'gold'; }
      else if (kind === 'open') { nr.complete = true; nr.gatewayOpened = true; text = 'The gateway answers. The keyed archipelago is open.'; tone = 'gold'; }
      return nr;
    });
    dispatch({ type: 'FIELD_CARE', ...care });
    setEnc(e => ({ ...e, phase: 'result', text, tone }));
  }

  function finishRun(success) {
    dispatch({ type: 'RETURN_PORT', haul: run.haul, rescues: run.recruits, success, daysLeft: state.manifestDays, unlockedKeyed: run.gatewayOpened });
    onReturnScreen && onReturnScreen();
  }

  const node = enc ? NODES[enc.nodeId] : null;
  const nt = node ? NTYPE[node.type] : null;
  const overlay = !!enc;

  return (
    <div className="phone sh-m">
      <AndroidStatus />
      <div className="appbar" style={{ paddingBottom: 6 }}>
        <div className="col">
          <span className="home" style={{ fontSize: 15 }}>Whispering Strata</span>
          <span className="sub">ZONE 0 · {SLOTS_LABEL(state.slotIdx)} · {state.manifestDays}d left</span>
        </div>
        <MiniDayDial day={state.day} slotIdx={state.slotIdx} size={48} />
      </div>

      {/* field condition: Hull + appetite + the run haul */}
      <div className="row g8 aic" style={{ padding: '0 14px 8px' }}>
        <span className="label" style={{ fontSize: 8.5, width: 28, flex: '0 0 28px' }}>Hull</span>
        <div className="meter" style={{ flex: 1 }}>
          {Array.from({ length: 10 }).map((_, i) => <div key={i} className={'meter-seg' + (i < Math.round(hull / 2) ? (hull <= 6 ? ' crit' : ' on') : '')} />)}
        </div>
        <span className="mono" style={{ fontSize: 10, width: 30, textAlign: 'right' }}>{hull}/{FC.C.HULL_MAX}</span>
        <span style={{ borderLeft: '1px solid var(--ink-faded)', height: 16, alignSelf: 'center' }} />
        <span className="mono muted" style={{ fontSize: 10 }}>♨{Math.round(state.care.fullness)} ⌖{run.haul.coins} ✦{run.recruits.length}</span>
      </div>

      {/* the map — drag to haul vertically; tap a lit node to travel */}
      <div ref={viewRef} className="screen-body" style={{ overflow: 'hidden', padding: '0 14px', position: 'relative', touchAction: 'none', cursor: grabbing ? 'grabbing' : 'grab' }}
        onPointerDown={onPanDown} onPointerMove={onPanMove} onPointerUp={onPanUp} onPointerCancel={onPanUp}>
        <div style={{ position: 'relative', width: MAP_W, height: MAP_H, margin: '0 auto', transform: `translateY(${offset}px)`, willChange: 'transform' }}>
          <svg viewBox={`0 0 ${MAP_W} ${MAP_H}`} width={MAP_W} height={MAP_H} style={{ position: 'absolute', inset: 0 }}>
            {EDGES.map(([a, b], i) => {
              const A = NODES[a], B = NODES[b];
              const walked = visited.includes(a) && visited.includes(b);
              const live = a === current && reachable.includes(b);
              return <line key={i} x1={A.x} y1={A.y} x2={B.x} y2={B.y}
                stroke={walked ? 'var(--ink)' : live ? 'var(--stamp-amber)' : 'var(--ink-ghost)'}
                strokeWidth={walked ? 2 : live ? 2 : 1} strokeOpacity={walked ? 0.7 : live ? 0.85 : 0.4}
                strokeDasharray={walked || live ? 'none' : '3 4'} />;
            })}
          </svg>
          {Object.entries(NODES).map(([id, n]) => {
            const t = NTYPE[n.type];
            const isCur = id === current, isVis = visited.includes(id), isReach = reachable.includes(id);
            const dim = !isCur && !isVis && !isReach;
            const sz = n.type === 'gateway' ? 64 : 50;
            return (
              <button key={id} data-node={id}
                style={{ position: 'absolute', left: n.x, top: n.y, transform: 'translate(-50%,-50%)', width: sz, height: sz, borderRadius: '50%', padding: 0, margin: 0,
                  border: `${n.type === 'gateway' ? 3 : 1.6}px ${t.boss ? 'double' : 'solid'} ${t.color}`,
                  background: t.filled ? 'var(--ink)' : 'var(--paper-light)', color: t.filled ? 'var(--paper-light)' : t.color,
                  fontFamily: 'IM Fell English SC, serif', fontSize: n.type === 'gateway' ? 28 : 20,
                  cursor: isReach ? 'pointer' : 'grab', touchAction: 'none', opacity: dim ? 0.4 : 1,
                  boxShadow: isCur ? '0 0 0 5px rgba(184,99,42,0.28)' : isReach ? '0 0 0 4px rgba(184,99,42,0.16)' : 'none',
                  transition: 'box-shadow 200ms ease, opacity 200ms ease', zIndex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                {isVis && !isCur ? '✓' : t.glyph}
                {isReach && <span style={{ position: 'absolute', inset: -3, borderRadius: '50%', animation: 'marker-pulse 1.6s ease-in-out infinite' }} />}
              </button>
            );
          })}
          {Object.entries(NODES).map(([id, n]) => (
            (id === current || reachable.includes(id)) && (
              <span key={'lbl' + id} className="fell" style={{ position: 'absolute', left: n.x, top: n.y + (n.type === 'gateway' ? 40 : 32), transform: 'translateX(-50%)', fontSize: 11, color: 'var(--ink)', whiteSpace: 'nowrap', pointerEvents: 'none' }}>{n.name}</span>
            )
          ))}
        </div>
      </div>

      {/* hint / husk / complete banner */}
      {!overlay && !complete && !husk && (
        <div className="row center" style={{ padding: '0 14px 10px' }}>
          <span className="label" style={{ color: 'var(--stamp-amber)' }}>▲ Drag the chart · tap a lit node</span>
        </div>
      )}
      {!overlay && husk && (
        <div className="col g8" style={{ padding: '6px 14px 12px' }}>
          <div className="panel" style={{ borderColor: 'var(--crit)', padding: '10px 13px' }}>
            <span className="fell" style={{ fontSize: 15, color: 'var(--crit)' }}>The clock ran dry</span>
            <span className="mono muted" style={{ fontSize: 10, display: 'block', marginTop: 3, lineHeight: 1.5 }}>The manifestation faded mid-run. The empty husk flees home with whatever it carried.</span>
          </div>
          <button className="btn-primary" style={{ fontSize: 15, padding: 12 }} onPointerDown={() => finishRun(false)}>The Husk Flees Home ▸</button>
        </div>
      )}
      {!overlay && complete && (
        <div className="col g8" style={{ padding: '6px 14px 12px' }}>
          <div className="panel brackets panel-pad col g4">
            <span className="fell" style={{ fontSize: 16 }}>Run complete</span>
            <span className="mono muted" style={{ fontSize: 10 }}>Banked: {run.haul.staple} staple · {run.haul.coins} coin · {run.recruits.length} recruit{run.recruits.length !== 1 ? 's' : ''}. Keyed archipelago unlocked.</span>
          </div>
          <button className="btn-primary" style={{ fontSize: 15, padding: 12 }} onPointerDown={() => finishRun(true)}>Return to the Hearth ▸</button>
        </div>
      )}

      {tabBar}

      {/* ENCOUNTER — covers the map */}
      {enc && node && (
        <>
          <div style={{ position: 'absolute', inset: 0, background: 'rgba(20,14,8,0.34)', zIndex: 40 }} onPointerDown={enc.phase === 'result' ? () => setEnc(null) : undefined} />
          <div className="sheet" style={{ position: 'absolute', left: 0, right: 0, bottom: 0, zIndex: 41, maxHeight: '72%', overflow: 'auto' }}>
            <div className="handle" />
            <div className="row g10 aic" style={{ marginBottom: 10 }}>
              <div style={{ width: 48, height: 48, flex: '0 0 48px', borderRadius: '50%', border: `2px ${nt.boss ? 'double' : 'solid'} ${nt.color}`, background: nt.filled ? 'var(--ink)' : 'var(--paper-light)', color: nt.filled ? 'var(--paper-light)' : nt.color, display: 'flex', alignItems: 'center', justifyContent: 'center', fontFamily: 'IM Fell English SC, serif', fontSize: 24 }}>{nt.glyph}</div>
              <div className="col grow">
                <span className="label" style={{ color: nt.color, fontSize: 9 }}>{nt.label}</span>
                <span className="fell" style={{ fontSize: 19, lineHeight: 1 }}>{node.name}</span>
              </div>
            </div>
            <span className="mono muted" style={{ fontSize: 11, lineHeight: 1.55, display: 'block', marginBottom: 12 }}>{node.blurb}</span>

            {enc.phase === 'choose' ? (
              <div className="col g7" style={{ gap: 7 }}>
                {node.type === 'forage' && <>
                  <EncBtn primary label={`Forage · Nature ${Math.round(Math.max(0.2, FC.checkChance(elv('nature'), 2, 10)) * 100)}%`} sub="pass → +4 staple · fail → −2 Hull" onTap={() => resolve(enc.nodeId, 'forage')} />
                  <EncBtn label="Press on" sub="leave it · spends the slot" onTap={() => resolve(enc.nodeId, 'skip')} />
                </>}
                {node.type === 'gate' && (elv('earth') >= 12
                  ? <EncBtn primary label={`Pass the tide-gate · Earth ${elv('earth')} ✓`} sub="requirement met" onTap={() => resolve(enc.nodeId, 'gate', 'pass')} />
                  : <>
                    <EncBtn label={`Earth ${elv('earth')} / 12 — the gate holds`} sub="requirement not met" disabled />
                    <EncBtn primary label="Force the gate" sub="−4 Hull" onTap={() => resolve(enc.nodeId, 'gate', 'force')} />
                  </>)}
                {node.type === 'danger' && <>
                  <EncBtn primary el={node.force} label={`Force it · ${SH_EL[node.force].name} ${Math.round(FC.checkChance(elv(node.force), node.band[0], node.band[1]) * 100)}%`} sub={`risky · fail → −${node.hullFail} Hull`} onTap={() => resolve(enc.nodeId, 'force')} />
                  <EncBtn label="Finesse the edge" sub="sure · −3 Hull" onTap={() => resolve(enc.nodeId, 'finesse')} />
                  <EncBtn label="Wait it down" sub={`sure · −${node.hullFail} Hull`} onTap={() => resolve(enc.nodeId, 'wait')} />
                </>}
                {node.type === 'recruit' && <EncBtn primary label={`Recruit the ${node.name}`} sub="joins the Hearth" onTap={() => resolve(enc.nodeId, 'recruit')} />}
                {node.type === 'treasure' && <EncBtn primary label="Claim the cache" sub="+6 coin · +1 offering" onTap={() => resolve(enc.nodeId, 'claim')} />}
                {node.type === 'gateway' && <EncBtn primary label="Open the Gateway" sub="unlocks the keyed archipelago" onTap={() => resolve(enc.nodeId, 'open')} />}
              </div>
            ) : (
              <div className="col g10">
                <div className="telem" style={{ fontSize: 12 }}><span className={enc.tone}>{enc.text}</span></div>
                <button className="btn-primary" style={{ fontSize: 15, padding: 11 }} onPointerDown={() => setEnc(null)}>
                  {NODES[enc.nodeId].type === 'gateway' ? 'Behold ▸' : 'Continue ▸'}
                </button>
              </div>
            )}
          </div>
        </>
      )}
    </div>
  );
}

function SLOTS_LABEL(i) { return ['NIGHT', 'MORNING', 'AFTERNOON', 'EVENING'][i] || ''; }

function EncBtn({ label, sub, onTap, primary, disabled, el }) {
  const color = el ? SH_EL[el].color : null;
  return (
    <button onPointerDown={disabled ? undefined : onTap} disabled={disabled}
      style={{ width: '100%', textAlign: 'left', display: 'flex', alignItems: 'center', gap: 10,
        border: `1.5px solid ${primary ? 'var(--ink)' : color || 'var(--ink-faded)'}`,
        background: primary ? 'var(--ink)' : 'rgba(244,236,214,0.5)', color: primary ? 'var(--paper-light)' : 'var(--ink)',
        padding: '10px 12px', borderRadius: 2, opacity: disabled ? 0.5 : 1, cursor: disabled ? 'default' : 'pointer' }}>
      {el && <span style={{ width: 26, height: 26, flex: '0 0 26px', borderRadius: '50%', border: `1.5px solid ${primary ? 'var(--paper-light)' : color}`, display: 'flex', alignItems: 'center', justifyContent: 'center' }}><img className="drop-white" src={SH_EL[el].icon} alt="" style={{ width: 18, height: 18 }} /></span>}
      <span className="col grow" style={{ gap: 2 }}>
        <span className="fell" style={{ fontSize: 14, lineHeight: 1.1 }}>{label}</span>
        {sub && <span className="mono" style={{ fontSize: 9, opacity: 0.72 }}>{sub}</span>}
      </span>
    </button>
  );
}

Object.assign(window, { FieldRoute, SH_NODES: NODES });
