// ─────── CORE: shared data shaping, routing, commands, key handling ───────

const PortfolioCore = (() => {
  const AUDIENCE = {
    GATE: '',
    SIMPLE: 'simple',
    NEOVIM: 'neovim',
  };

  const TWEAK_DEFAULTS = {
    theme: 'dark',
    accent: 'blue',
    tree: true,
  };

  const CONTACT_FORM_DEFAULT = {
    name: '',
    from: '',
    subject: 'Hello from your portfolio',
    msg: '',
  };

  const ACCENT_SWATCHES = [
    { name: 'blue', dark: '#6aa9ff', light: '#2d66c9' },
    { name: 'green', dark: '#88d488', light: '#4a8f4a' },
    { name: 'purple', dark: '#c5a0ff', light: '#8c4fc7' },
    { name: 'orange', dark: '#f0a868', light: '#c46a1f' },
    { name: 'pink', dark: '#e88bcc', light: '#b34f92' },
    { name: 'cyan', dark: '#7adbd1', light: '#2e8a82' },
  ];

  const SIMPLE_SECTION_IDS = ['about', 'now', 'projects', 'experience', 'writing', 'education', 'skills'];
  const SIMPLE_LABELS = {
    about: 'About',
    now: 'Now',
    projects: 'Projects',
    experience: 'Experience',
    writing: 'Writing',
    education: 'Education',
    skills: 'Skills',
  };
  const SIMPLE_SECTIONS = SIMPLE_SECTION_IDS.map((id) => ({ id, label: SIMPLE_LABELS[id] || id }));

  const KEY_GROUPS = [
    {
      group: 'navigation',
      items: [
        { key: 'h / k', action: 'move cursor to previous object' },
        { key: 'j / l', action: 'move cursor to next object' },
        { key: 'gg', action: 'jump to top of buffer' },
        { key: 'G', action: 'jump to end of buffer' },
        { key: '1…6', action: 'jump to buffer N' },
        { key: '⇥', action: 'next buffer' },
      ],
    },
    {
      group: 'actions',
      items: [
        { key: '⏎', action: 'activate focused object' },
        { key: 'o', action: 'open project link / next article' },
        { key: 'O', action: 'open previous article (writing buffer)' },
        { key: 'i/a/o/O', action: 'contact: enter insert on editable field' },
        { key: '/', action: 'search within buffer' },
        { key: 'n / N', action: 'next / prev search match' },
        { key: ':', action: 'command mode' },
        { key: '?', action: 'toggle this which-key' },
        { key: '⎋', action: 'close popup / cancel' },
        { key: 't', action: 'toggle theme (dark/light)' },
      ],
    },
    {
      group: 'commands',
      items: [
        { key: ':help', action: 'show which-key' },
        { key: ':q', action: 'close popup' },
        { key: ':send / :mail', action: 'send email from contact buffer' },
        { key: ':w / :wq', action: 'contact: show send hint' },
        { key: ':projects', action: 'open projects buffer' },
        { key: ':about', action: 'open about' },
        { key: ':now', action: 'open now' },
        { key: ':contact', action: 'open contact' },
        { key: ':gh', action: 'open github profile' },
        { key: ':cv', action: 'email me directly' },
      ],
    },
    {
      group: 'ui',
      items: [
        { key: ':set bg=dark', action: 'switch to dark theme' },
        { key: ':set bg=light', action: 'switch to light theme' },
        { key: ':settings', action: 'open settings panel' },
        { key: ':NvimTree', action: 'toggle file tree' },
        { key: 'zz', action: 'center cursor (scroll to middle)' },
      ],
    },
  ];

  const BASE_COMMANDS = [
    'q', 'quit', 'close',
    'w', 'write', 'wq', 'x',
    'help', 'h', '?',
    'settings', 'tweaks', 'set',
    'nvimtree', 'tree',
    'gh', 'github',
    'li', 'linkedin',
    'cv', 'mail', 'email',
    'send',
    'dark', 'light',
    'set bg=dark', 'set bg=light',
  ];

  const MONTHS_SHORT = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

  function normalizeAudience(value) {
    if (value === 'developer') return AUDIENCE.NEOVIM;
    if (value === 'recruiter') return AUDIENCE.SIMPLE;
    if (value === AUDIENCE.NEOVIM || value === AUDIENCE.SIMPLE) return value;
    return AUDIENCE.GATE;
  }

  function readJson(key, fallback) {
    try {
      const saved = localStorage.getItem(key);
      return saved ? JSON.parse(saved) : fallback;
    } catch {
      return fallback;
    }
  }

  function writeJson(key, value) {
    try {
      localStorage.setItem(key, JSON.stringify(value));
    } catch {}
  }

  function readText(key, fallback = '') {
    try {
      return localStorage.getItem(key) || fallback;
    } catch {
      return fallback;
    }
  }

  function writeText(key, value) {
    try {
      localStorage.setItem(key, value);
    } catch {}
  }

  function renderInline(text) {
    const parts = String(text).split(/(\*\*[^*]+\*\*)/g);
    return parts.map((part, i) =>
      part.startsWith('**') && part.endsWith('**') ? (
        <b key={i}>{part.slice(2, -2)}</b>
      ) : (
        <React.Fragment key={i}>{part}</React.Fragment>
      )
    );
  }

  function escapeRegExp(text) {
    return String(text).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  }

  function renderSearchText(text, query, isActive) {
    if (!query || !query.trim()) return text;
    const q = query.trim();
    const rx = new RegExp(`(${escapeRegExp(q)})`, 'ig');
    const parts = String(text).split(rx);
    return parts.map((part, i) =>
      part.toLowerCase() === q.toLowerCase() ? (
        <mark key={i} className={'vs-hl' + (isActive ? ' active' : '')}>
          {part}
        </mark>
      ) : (
        <React.Fragment key={i}>{part}</React.Fragment>
      )
    );
  }

  function formatPostDate(iso) {
    if (!iso) return '';
    const [y, m, d] = iso.split('-').map(Number);
    return `${d} ${MONTHS_SHORT[m - 1]} ${y}`;
  }

  function getPosts(posts) {
    return (posts || [])
      .slice()
      .filter((p) => p.title && p.date)
      .sort((a, b) => b.date.localeCompare(a.date));
  }

  function getProjectSearchMatches(data, query) {
    if (!query || !query.trim()) return [];
    const q = query.toLowerCase();
    return data.projects
      .map((p, i) => ({ p, i }))
      .filter(({ p }) => (p.name + p.blurb + p.stack.join(' ')).toLowerCase().includes(q))
      .map(({ i }) => i);
  }

  function parseHash(hash = window.location.hash) {
    const raw = String(hash).replace(/^#/, '');
    const [bufferId = '', slug = ''] = raw.split('/');
    return { raw, bufferId, slug: slug ? decodeURIComponent(slug) : '' };
  }

  function parseWritingSlug(hash = window.location.hash) {
    const { bufferId, slug } = parseHash(hash);
    return bufferId === 'writing' && slug ? slug : null;
  }

  function hashForBuffer(bufferId, slug) {
    return `#${bufferId}${slug ? `/${encodeURIComponent(slug)}` : ''}`;
  }

  function resolveBuffer(buffers, command) {
    const c = String(command || '').toLowerCase();
    return buffers.find((b) => b.id === c || b.file === c || b.file === `${c}.md`) || null;
  }

  function getCommandCompletions(buffers) {
    return [
      ...BASE_COMMANDS,
      ...buffers.map((b) => b.id),
      ...buffers.map((b) => b.file),
    ].sort();
  }

  function buildMailto(data, form) {
    const name = (form.name || '').trim() || 'Portfolio visitor';
    const from = (form.from || '').trim();
    const subject =
      (form.subject || '').trim() ||
      `portfolio: hello from ${name === 'Portfolio visitor' ? 'a visitor' : name}`;
    const body = (form.msg || '').trim();

    if (!from || !/^\S+@\S+\.\S+$/.test(from) || !body) {
      return { ok: false, error: 'contact form invalid: add valid email + message before sending' };
    }

    const signature = `\n\n- ${name} <${from}>`;
    const mailBody = `${body}${signature}`;
    return {
      ok: true,
      url: `mailto:${data.email}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(mailBody)}`,
    };
  }

  function runCommand(raw, ctx) {
    const c = raw.trim().toLowerCase();
    if (!c) return;

    if (c === 'w' || c === 'write' || c === 'wq' || c === 'x') {
      ctx.flash(ctx.activeBuffer === 'contact' ? '-- to send: click Send Email or run :send/:mail --' : 'E32: no file name', 1800);
      return;
    }
    if (c === 'send' || (c === 'mail' && ctx.activeBuffer === 'contact')) {
      if (ctx.activeBuffer === 'contact') ctx.sendContactForm();
      else ctx.flash('E492: :send is available only in contact buffer', 1900);
      return;
    }
    if (c === 'q' || c === 'quit' || c === 'close') {
      ctx.setShowTweaks(false);
      ctx.setShowWhichKey(false);
      return;
    }
    if (c === 'help' || c === 'h' || c === '?') {
      ctx.setShowWhichKey(true);
      return;
    }
    if (c === 'tweaks' || c === 'settings' || c === 'set') {
      ctx.setShowTweaks(true);
      return;
    }
    if (c === 'nvimtree' || c === 'tree') {
      ctx.setTweaks((t) => ({ ...t, tree: !t.tree }));
      return;
    }
    if (c === 'gh' || c === 'github') {
      window.open(`https://${ctx.data.github}`, '_blank');
      return;
    }
    if (c === 'li' || c === 'linkedin') {
      window.open(`https://${ctx.data.linkedin}`, '_blank');
      return;
    }
    if (c === 'cv' || c === 'mail' || c === 'email') {
      window.location.href = `mailto:${ctx.data.email}`;
      return;
    }
    if (c.startsWith('set bg=')) {
      const v = c.split('=')[1];
      if (v === 'dark' || v === 'light') ctx.setTweaks((t) => ({ ...t, theme: v }));
      return;
    }
    if (c === 'dark' || c === 'light') {
      ctx.setTweaks((t) => ({ ...t, theme: c }));
      return;
    }

    const buffer = resolveBuffer(ctx.buffers, c);
    if (buffer) {
      ctx.gotoBuffer(buffer.id);
      return;
    }

    ctx.flash(`E492: not an editor command: ${raw}`, 2000);
  }

  function scrollFocusedItemIntoView(ctx) {
    const el = ctx.bufferRef.current;
    if (!el) return;
    if (ctx.activeBuffer === 'projects' && ctx.focusIdx < 0) {
      el.scrollTo({ top: 0, behavior: 'smooth' });
      return;
    }
    const item = el.querySelector('[data-focus="true"]');
    if (!item) return;
    const r = item.getBoundingClientRect();
    const p = el.getBoundingClientRect();
    if (r.top < p.top + 80) el.scrollBy({ top: r.top - p.top - 80, behavior: 'smooth' });
    else if (r.bottom > p.bottom - 40) el.scrollBy({ top: r.bottom - p.bottom + 40, behavior: 'smooth' });
  }

  function handleNvimKey(e, ctx) {
    if (ctx.audience !== AUDIENCE.NEOVIM) return;
    if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
    if (ctx.mode === 'COMMAND' || ctx.mode === 'SEARCH') return;

    if (e.key === '?') {
      e.preventDefault();
      ctx.setShowWhichKey((s) => !s);
      return;
    }
    if (ctx.showWhichKey) return;

    if (e.key === 'Escape') {
      ctx.setExpanded(null);
      ctx.setSearch('');
      ctx.setPending('');
      ctx.setShowTweaks(false);
      return;
    }

    if (e.key === ':') {
      e.preventDefault();
      ctx.setMode('COMMAND');
      ctx.setCmd('');
      return;
    }
    if (e.key === '/') {
      e.preventDefault();
      ctx.setMode('SEARCH');
      ctx.setCmd('');
      return;
    }

    if (ctx.activeBuffer === 'contact') {
      if (e.key === 'i' || e.key === 'I' || e.key === 'a' || e.key === 'A') {
        e.preventDefault();
        ctx.requestContactFocus('edit_current');
        return;
      }
      if (e.key === 'o') {
        e.preventDefault();
        ctx.requestContactFocus('edit_next');
        return;
      }
      if (e.key === 'O') {
        e.preventDefault();
        ctx.requestContactFocus('edit_prev');
        return;
      }
      if (e.key === 'j' || e.key === 'l') {
        e.preventDefault();
        ctx.requestContactFocus('move_next');
        return;
      }
      if (e.key === 'k' || e.key === 'h') {
        e.preventDefault();
        ctx.requestContactFocus('move_prev');
        return;
      }
      if (e.key === 'Enter') {
        e.preventDefault();
        ctx.requestContactFocus('activate');
        return;
      }
    }

    if (/^[1-9]$/.test(e.key)) {
      const n = parseInt(e.key, 10);
      if (ctx.buffers[n - 1]) {
        e.preventDefault();
        ctx.gotoBuffer(ctx.buffers[n - 1].id);
        return;
      }
    }

    if (e.key === 'Tab') {
      e.preventDefault();
      const next = (ctx.bufferIdx + (e.shiftKey ? -1 : 1) + ctx.buffers.length) % ctx.buffers.length;
      ctx.gotoBuffer(ctx.buffers[next].id);
      return;
    }

    const el = ctx.bufferRef.current;
    const moveContentCursor = (delta) => {
      if (ctx.activeBuffer === 'projects') {
        ctx.setFocusIdx((i) => Math.max(-1, Math.min(ctx.data.projects.length - 1, i + delta)));
        setTimeout(() => scrollFocusedItemIntoView(ctx), 0);
        return true;
      }
      if (ctx.activeBuffer === 'experience') {
        ctx.setFocusIdx((i) => Math.max(0, Math.min(ctx.data.experience.length - 1, i + delta)));
        setTimeout(() => scrollFocusedItemIntoView(ctx), 0);
        return true;
      }
      if (ctx.activeBuffer === 'writing' && /^#writing\/.+/.test(window.location.hash)) {
        if (el) el.scrollBy({ top: delta * 80, behavior: 'smooth' });
        return true;
      }

      if (!el) return false;
      const items = Array.from(el.querySelectorAll('a, button')).filter((node) => node.offsetParent !== null);
      if (!items.length) return false;

      const current = items.findIndex((node) => node === document.activeElement);
      const base = current === -1 ? (delta > 0 ? -1 : 0) : current;
      const next = (base + delta + items.length) % items.length;
      const target = items[next];
      if (target && typeof target.focus === 'function') {
        target.focus();
        if (typeof target.scrollIntoView === 'function') target.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
        return true;
      }
      return false;
    };

    const cycleSearchMatch = (delta) => {
      if (ctx.activeBuffer !== 'projects' || !ctx.search.trim()) {
        ctx.flash('E35: no previous search pattern', 1200);
        return;
      }
      if (!ctx.projectSearchMatches.length) {
        ctx.flash(`E486: Pattern not found: ${ctx.search}`, 1400);
        return;
      }
      ctx.setSearchMatchCursor((c) => {
        const len = ctx.projectSearchMatches.length;
        const next = ((c + delta) % len + len) % len;
        ctx.setFocusIdx(ctx.projectSearchMatches[next]);
        setTimeout(() => scrollFocusedItemIntoView(ctx), 0);
        return next;
      });
    };

    switch (e.key) {
      case 'h':
      case 'k':
        e.preventDefault();
        moveContentCursor(-1);
        break;
      case 'j':
      case 'l':
        e.preventDefault();
        moveContentCursor(1);
        break;
      case 'n':
        e.preventDefault();
        cycleSearchMatch(1);
        break;
      case 'N':
        e.preventDefault();
        cycleSearchMatch(-1);
        break;
      case 'g':
        e.preventDefault();
        if (ctx.pending === 'g') {
          if (el) el.scrollTo({ top: 0, behavior: 'smooth' });
          ctx.setPending('');
        } else {
          ctx.setPending('g');
          ctx.flash('g… (gg to top)', 1000);
          setTimeout(() => ctx.setPending((p) => (p === 'g' ? '' : p)), 1000);
        }
        break;
      case 'G':
        e.preventDefault();
        if (el) el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' });
        break;
      case 'Enter': {
        e.preventDefault();
        if (ctx.activeBuffer === 'projects') {
          const p = ctx.data.projects[ctx.focusIdx];
          if (p) ctx.setExpanded((e2) => (e2 === p.id ? null : p.id));
        } else {
          const ae = document.activeElement;
          if (ae && (ae.tagName === 'A' || ae.tagName === 'BUTTON') && typeof ae.click === 'function') ae.click();
        }
        break;
      }
      case 'o': {
        e.preventDefault();
        if (ctx.activeBuffer === 'projects') {
          const p = ctx.data.projects[ctx.focusIdx];
          if (p) window.open(p.link, '_blank');
        }
        break;
      }
      case 't':
        e.preventDefault();
        ctx.setTweaks((t) => ({ ...t, theme: t.theme === 'dark' ? 'light' : 'dark' }));
        break;
      default:
        break;
    }
  }

  function createDeps({ data, buffers, posts }) {
    const normalizedPosts = getPosts(posts);
    return {
      data,
      buffers,
      posts: normalizedPosts,
      constants: {
        AUDIENCE,
        TWEAK_DEFAULTS,
        CONTACT_FORM_DEFAULT,
        ACCENT_SWATCHES,
        SIMPLE_SECTIONS,
      },
      utils: { renderInline, renderSearchText, escapeRegExp, formatPostDate },
      selectors: { getPosts, getProjectSearchMatches },
      routing: { parseHash, parseWritingSlug, hashForBuffer, resolveBuffer },
      storage: { readJson, writeJson, readText, writeText },
      actions: { buildMailto },
      commands: { run: runCommand, completions: getCommandCompletions(buffers) },
      keymap: { groups: KEY_GROUPS, handleNvimKey },
    };
  }

  return {
    constants: { AUDIENCE, TWEAK_DEFAULTS, CONTACT_FORM_DEFAULT, ACCENT_SWATCHES, SIMPLE_SECTIONS },
    utils: { renderInline, renderSearchText, escapeRegExp, formatPostDate },
    selectors: { getPosts, getProjectSearchMatches },
    routing: { parseHash, parseWritingSlug, hashForBuffer, resolveBuffer },
    storage: { readJson, writeJson, readText, writeText },
    actions: { buildMailto },
    commands: { run: runCommand, getCompletions: getCommandCompletions },
    keymap: { groups: KEY_GROUPS, handleNvimKey },
    normalizeAudience,
    createDeps,
  };
})();

window.PortfolioCore = PortfolioCore;
