// ─────── APP: wires everything, handles vim motions ───────

function App() {
  const deps = React.useMemo(
    () => PortfolioCore.createDeps({ data: DATA, buffers: BUFFERS, posts: window.POSTS || [] }),
    []
  );
  const { data, buffers, constants, routing, selectors, storage, actions, commands, keymap } = deps;
  const { AUDIENCE, TWEAK_DEFAULTS, CONTACT_FORM_DEFAULT } = constants;

  // vim state
  const [mode, setMode] = React.useState('NORMAL');
  const [cmd, setCmd] = React.useState('');
  const [activeBuffer, setActiveBuffer] = React.useState(() => {
    return storage.readText('buf', 'projects');
  });
  const [focusIdx, setFocusIdx] = React.useState(0);
  const [expanded, setExpanded] = React.useState(null);
  const [showWhichKey, setShowWhichKey] = React.useState(false);
  const [showTweaks, setShowTweaks] = React.useState(false);
  const [hint, setHint] = React.useState('');
  const [search, setSearch] = React.useState('');
  const [searchMatchCursor, setSearchMatchCursor] = React.useState(0);
  const [pending, setPending] = React.useState(''); // for gg
  const [line, setLine] = React.useState(1);
  const [audience, setAudience] = React.useState(() => {
    const params = new URLSearchParams(window.location.search);
    const requested = PortfolioCore.normalizeAudience(params.get('view'));
    if (requested) {
      storage.writeText('audience-view', requested);
      return requested;
    }
    const stored = storage.readText('audience-view', '');
    const next = PortfolioCore.normalizeAudience(stored);
    if (stored && stored !== next) storage.writeText('audience-view', next);
    if (next) return next;
    // no stored choice yet: on phones/tablets, skip the gate and land on simple
    try {
      if (window.matchMedia && window.matchMedia('(max-width: 720px)').matches) {
        return AUDIENCE.SIMPLE;
      }
    } catch {}
    return AUDIENCE.GATE;
  });
  const [showSplash, setShowSplash] = React.useState(() => {
    return !storage.readText('seen-splash', '');
  });

  // tweaks
  const [tweaks, setTweaks] = React.useState(() => {
    return { ...TWEAK_DEFAULTS, ...storage.readJson('tweaks', {}) };
  });
  const [contactForm, setContactForm] = React.useState(() => {
    return { ...CONTACT_FORM_DEFAULT, ...storage.readJson('contact-form', {}) };
  });
  const [contactFocusTick, setContactFocusTick] = React.useState(0);
  const [contactFocusAction, setContactFocusAction] = React.useState('current');

  const bufferRef = React.useRef(null);

  const chooseAudience = (next, bufferId, slug) => {
    const normalized = PortfolioCore.normalizeAudience(next);
    setAudience(normalized);
    storage.writeText('audience-view', normalized);
    if (bufferId) gotoBuffer(bufferId, slug);
  };

  // toggle page scroll for non-neovim views
  React.useEffect(() => {
    document.documentElement.setAttribute('data-audience', audience || 'gate');
  }, [audience]);

  // apply theme
  React.useEffect(() => {
    document.documentElement.setAttribute('data-theme', tweaks.theme);
    const sw = constants.ACCENT_SWATCHES.find((s) => s.name === tweaks.accent);
    if (sw) {
      const color = tweaks.theme === 'light' ? sw.light : sw.dark;
      document.documentElement.style.setProperty('--status', color);
      document.documentElement.style.setProperty('--blue', color);
    }
    storage.writeJson('tweaks', tweaks);
  }, [tweaks]);

  // persist active buffer
  React.useEffect(() => {
    storage.writeText('buf', activeBuffer);
    setFocusIdx(0);
    setExpanded(null);
    setLine(1);
    setSearch('');
    setMode('NORMAL');
    if (bufferRef.current) bufferRef.current.scrollTop = 0;
    // sync hash with active buffer (but don't clobber a post deep link)
    const curHash = window.location.hash.slice(1);
    const [curBuf] = curHash.split('/');
    if (curBuf !== activeBuffer) {
      history.replaceState(null, '', routing.hashForBuffer(activeBuffer));
    }
  }, [activeBuffer]);

  // hash routing: #<bufferId>[/<slug>] selects buffer (and post, for writing)
  React.useEffect(() => {
    const apply = () => {
      const h = window.location.hash.slice(1);
      if (!h) return;
      const [bufId] = h.split('/');
      if (buffers.some((b) => b.id === bufId)) setActiveBuffer(bufId);
    };
    // initial hash on mount overrides the localStorage default
    const h = window.location.hash.slice(1);
    if (h) {
      const [bufId] = h.split('/');
      if (buffers.some((b) => b.id === bufId) && bufId !== activeBuffer) {
        setActiveBuffer(bufId);
      }
    }
    window.addEventListener('hashchange', apply);
    return () => window.removeEventListener('hashchange', apply);
  }, []);

  React.useEffect(() => {
    storage.writeJson('contact-form', contactForm);
  }, [contactForm]);

  // splash timeout
  React.useEffect(() => {
    if (showSplash && audience === AUDIENCE.NEOVIM) {
      const t = setTimeout(() => {
        setShowSplash(false);
        storage.writeText('seen-splash', '1');
      }, 2700);
      return () => clearTimeout(t);
    }
  }, [showSplash, audience]);

  // track scroll → fake cursor line
  React.useEffect(() => {
    const el = bufferRef.current;
    if (!el) return;
    const onScroll = () => {
      const ln = Math.max(1, Math.floor(el.scrollTop / 21.7) + 1);
      setLine(ln);
    };
    el.addEventListener('scroll', onScroll);
    return () => el.removeEventListener('scroll', onScroll);
  }, [activeBuffer]);

  const bufferIdx = buffers.findIndex((b) => b.id === activeBuffer);
  const buffer = buffers[bufferIdx] || buffers[0];
  const projectSearchMatches = React.useMemo(() => {
    if (activeBuffer !== 'projects' || !search.trim()) return [];
    return selectors.getProjectSearchMatches(data, search);
  }, [activeBuffer, search]);

  const gotoBuffer = (id, slug) => {
    if (id && buffers.find((b) => b.id === id)) {
      setActiveBuffer(id);
      if (slug) window.location.hash = routing.hashForBuffer(id, slug);
      setHint(`-- opened ${id}.md --`);
      setTimeout(() => setHint(''), 1400);
    }
  };

  React.useEffect(() => {
    setSearchMatchCursor(0);
    if (activeBuffer === 'projects' && search.trim() && projectSearchMatches.length) {
      setFocusIdx(projectSearchMatches[0]);
    }
  }, [activeBuffer, search, projectSearchMatches]);

  const requestContactFocus = (action) => {
    setContactFocusAction(action);
    setContactFocusTick((n) => n + 1);
  };

  const sendContactForm = () => {
    const result = actions.buildMailto(data, contactForm);
    if (!result.ok) {
      flash(result.error, 2200);
      return;
    }

    window.location.href = result.url;
    flash('-- email draft opened in your mail client --', 1800);
  };

  const flash = (message, ms = 1600) => {
    setHint(message);
    setTimeout(() => setHint(''), ms);
  };

  const runCommand = (raw) => {
    commands.run(raw, {
      activeBuffer,
      data,
      buffers,
      setTweaks,
      setShowTweaks,
      setShowWhichKey,
      gotoBuffer,
      sendContactForm,
      flash,
    });
  };

  // key handler
  React.useEffect(() => {
    const handler = (e) => {
      keymap.handleNvimKey(e, {
        audience,
        mode,
        activeBuffer,
        bufferIdx,
        focusIdx,
        pending,
        showWhichKey,
        search,
        projectSearchMatches,
        data,
        buffers,
        bufferRef,
        setMode,
        setCmd,
        setExpanded,
        setSearch,
        setPending,
        setShowTweaks,
        setShowWhichKey,
        setFocusIdx,
        setSearchMatchCursor,
        setTweaks,
        requestContactFocus,
        gotoBuffer,
        flash,
      });
    };

    window.addEventListener('keydown', handler);
    return () => window.removeEventListener('keydown', handler);
  }, [audience, mode, activeBuffer, bufferIdx, focusIdx, pending, showWhichKey, search, projectSearchMatches]);

  const activeSearchProjectIdx =
    search.trim() && projectSearchMatches.length
      ? projectSearchMatches[
          ((searchMatchCursor % projectSearchMatches.length) + projectSearchMatches.length) %
            projectSearchMatches.length
        ]
      : -1;

  const lineCount = 160;
  const linums = [];
  for (let i = 1; i <= lineCount; i++) {
    const rel = Math.abs(i - line);
    linums.push(
      <span key={i} className={'ln' + (i === line ? ' current' : '')}>
        {rel}
      </span>
    );
  }

  const renderBuffer = () => {
    switch (activeBuffer) {
      case 'projects':
        return (
          <ProjectsBuffer
            deps={deps}
            focusIdx={focusIdx}
            setFocusIdx={setFocusIdx}
            expanded={expanded}
            setExpanded={setExpanded}
            searchQuery={search}
            activeSearchProjectIdx={activeSearchProjectIdx}
          />
        );
      case 'experience':
        return <ExperienceBuffer deps={deps} focusIdx={focusIdx} setFocusIdx={setFocusIdx} />;
      case 'about':
        return <AboutBuffer deps={deps} />;
      case 'now':
        return <NowBuffer deps={deps} />;
      case 'writing':
        return <WritingBuffer deps={deps} />;
      case 'contact':
        return (
          <ContactBuffer
            deps={deps}
            form={contactForm}
            setForm={setContactForm}
            onSend={sendContactForm}
            setMode={setMode}
            focusTick={contactFocusTick}
            focusAction={contactFocusAction}
          />
        );
      default:
        return (
          <ProjectsBuffer
            deps={deps}
            focusIdx={focusIdx}
            setFocusIdx={setFocusIdx}
            expanded={expanded}
            setExpanded={setExpanded}
            searchQuery={search}
            activeSearchProjectIdx={activeSearchProjectIdx}
          />
        );
    }
  };

  if (!audience) {
    return <AudienceGate deps={deps} onChoose={chooseAudience} />;
  }

  if (audience === AUDIENCE.SIMPLE) {
    return (
      <SimplePortfolio
        deps={deps}
        onSwitch={(bufferId, slug) => chooseAudience(AUDIENCE.NEOVIM, bufferId, slug)}
      />
    );
  }

  return (
    <div className="nvim">
      <button type="button" className="view-switch dev-switch" onClick={() => chooseAudience(AUDIENCE.SIMPLE)}>
        simple view
      </button>
      <div className="mobile-nvim-banner" role="status">
        <span className="mobile-nvim-banner-text">
          <strong>⚠ neovim mode isn't optimized for mobile.</strong>
          <small>keyboard-driven. tiny screens make navigation cramped.</small>
        </span>
        <button
          type="button"
          className="mobile-nvim-banner-btn"
          onClick={() => chooseAudience(AUDIENCE.SIMPLE)}
        >
          switch to simple view →
        </button>
      </div>
      {showSplash && <Splash />}

      <Tabline buffers={buffers} active={activeBuffer} onSelect={gotoBuffer} modified={{}} />
      <Winbar buffer={buffer} />

      <div className={'split' + (tweaks.tree ? '' : ' no-tree')}>
        {tweaks.tree && (
          <FileTree deps={deps} buffers={buffers} active={activeBuffer} onSelect={gotoBuffer} />
        )}

        <div className="buffer-wrap">
          <div className="linum">{linums}</div>
          <div className="buffer" ref={bufferRef} data-screen-label={buffer.file}>
            {renderBuffer()}
          </div>
        </div>
      </div>

      <Statusline mode={mode} buffer={buffer} line={line} total={lineCount} search={search} />
      <Cmdline
        mode={mode}
        cmd={cmd}
        setCmd={setCmd}
        onSubmit={() => {
          if (mode === 'COMMAND') runCommand(cmd);
          if (mode === 'SEARCH') setSearch(cmd);
          setCmd('');
          setMode('NORMAL');
        }}
        onCancel={() => {
          setCmd('');
          setMode('NORMAL');
        }}
        hint={hint}
        completions={commands.completions}
      />

      {showWhichKey && <WhichKey deps={deps} onClose={() => setShowWhichKey(false)} />}

      {showTweaks && (
        <TweaksPanel tweaks={tweaks} setTweaks={setTweaks} onClose={() => setShowTweaks(false)} />
      )}
    </div>
  );
}

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