/* global React, Icon, Badge, IconBtn, useLiveQuery, useReceiveFrom, SendToButton, relTime, db */
const { useState, useEffect, useRef, useMemo, useCallback } = React;

// ============================================================
// JSON Viewer — optimized for large documents
//
// Strategy for performance:
//   - Parse once on input change (debounced)
//   - Tree renders only EXPANDED nodes (collapsed children skipped entirely)
//   - Children rendered with "show first 100" pagination on big arrays
//   - Auto-collapse depth > 1 on initial load for large docs
//   - Syntax highlighting on the raw editor uses pre-rendered HTML, not per-line highlighting
// ============================================================

const PAGE_SIZE = 100;

function tryParse(text) {
  if (!text || !text.trim()) return { ok: true, value: null };
  try {
    return { ok: true, value: JSON.parse(text) };
  } catch (e) {
    // Locate error position from message ("Unexpected token ... at position N" or line/column)
    let line = null, col = null, msg = e.message;
    const posMatch = msg.match(/position (\d+)/);
    if (posMatch) {
      const pos = parseInt(posMatch[1], 10);
      const before = text.slice(0, pos);
      line = before.split('\n').length;
      col = pos - before.lastIndexOf('\n');
    }
    const lcMatch = msg.match(/line (\d+) column (\d+)/);
    if (lcMatch) { line = parseInt(lcMatch[1], 10); col = parseInt(lcMatch[2], 10); }
    return { ok: false, error: msg, line, col };
  }
}

function jsonType(v) {
  if (v === null) return 'null';
  if (Array.isArray(v)) return 'array';
  return typeof v;
}

function nodeStats(v) {
  const t = jsonType(v);
  if (t === 'array') return { type: 'array', size: v.length };
  if (t === 'object') return { type: 'object', size: Object.keys(v).length };
  return { type: t };
}

// Simple JSONPath subset:
//   $       → root
//   .key    → child
//   [n]     → index
//   [*]     → all
//   ..key   → recursive descent
function evalPath(root, path) {
  if (!path || !path.trim()) return [root];
  let p = path.trim();
  const hadDollar = p.startsWith('$');
  if (hadDollar) p = p.slice(1);

  // Must start with $ alone, or with . / [
  if (p.length > 0 && p[0] !== '.' && p[0] !== '[') {
    throw new Error('Path must start with $, . or [');
  }

  // Tokenize — strict: throw on any unrecognized character
  const tokens = [];
  let i = 0;
  while (i < p.length) {
    const c = p[i];
    if (c === '.') {
      if (p[i+1] === '.') {
        // recursive descent .. then optional key
        const m = p.slice(i+2).match(/^([\w-]+)/);
        if (m) { tokens.push({ kind: 'descent', key: m[1] }); i += 2 + m[1].length; continue; }
        // .. without key is meaningful only if followed by [*] or [n]
        if (p[i+2] === '[') { tokens.push({ kind: 'descent', key: '*' }); i += 2; continue; }
        throw new Error('".." must be followed by a key, [*], or [index]');
      }
      const m = p.slice(i+1).match(/^([\w-]+)/);
      if (!m) throw new Error('"." must be followed by a key name');
      tokens.push({ kind: 'key', key: m[1] });
      i += 1 + m[1].length;
    } else if (c === '[') {
      const end = p.indexOf(']', i);
      if (end < 0) throw new Error('Missing closing "]"');
      const inner = p.slice(i+1, end).trim();
      if (!inner) throw new Error('Empty "[]" — use [*] for all, [n] for index, or ["key"]');
      if (inner === '*') tokens.push({ kind: 'all' });
      else if (/^-?\d+$/.test(inner)) tokens.push({ kind: 'index', i: parseInt(inner, 10) });
      else if (/^"(.+)"$/.test(inner) || /^'(.+)'$/.test(inner)) tokens.push({ kind: 'key', key: inner.slice(1,-1) });
      else if (/^[\w-]+$/.test(inner)) tokens.push({ kind: 'key', key: inner });
      else throw new Error(`Invalid bracket expression "[${inner}]"`);
      i = end + 1;
    } else if (c === ' ') {
      i++;
    } else {
      throw new Error(`Unexpected "${c}" at position ${i} — paths look like $.items[0].name`);
    }
  }

  let cur = [root];
  for (const t of tokens) {
    const next = [];
    for (const v of cur) {
      if (v == null) continue;
      if (t.kind === 'key') {
        if (typeof v === 'object' && !Array.isArray(v)) {
          if (t.key in v) next.push(v[t.key]);
        }
      } else if (t.kind === 'index') {
        if (Array.isArray(v)) {
          const idx = t.i < 0 ? v.length + t.i : t.i;
          if (idx in v) next.push(v[idx]);
        }
      } else if (t.kind === 'all') {
        if (Array.isArray(v)) next.push(...v);
        else if (v && typeof v === 'object') next.push(...Object.values(v));
      } else if (t.kind === 'descent') {
        const walk = (x) => {
          if (x && typeof x === 'object') {
            if (t.key === '*' || (Array.isArray(x) ? false : (t.key in x))) {
              if (t.key === '*') {
                if (Array.isArray(x)) next.push(...x);
                else next.push(...Object.values(x));
              } else next.push(x[t.key]);
            }
            const children = Array.isArray(x) ? x : Object.values(x);
            for (const c of children) walk(c);
          }
        };
        walk(v);
      }
    }
    cur = next;
  }
  return cur;
}

// ============================================================
// Tree renderer (recursive, but only expanded children render)
// ============================================================
function JsonNode({ name, value, depth, expanded, setExpanded, path, search, matchSet, defaultOpen }) {
  const t = jsonType(value);
  const isContainer = t === 'object' || t === 'array';
  const isOpen = expanded[path] !== undefined ? expanded[path] : (depth < defaultOpen);
  const stats = isContainer ? nodeStats(value) : null;
  const matched = matchSet && matchSet.has(path);

  const toggle = () => {
    if (!isContainer) return;
    setExpanded(e => ({ ...e, [path]: !isOpen }));
  };

  const keyEl = name !== undefined ? (
    <span className="jv-key" data-matched={matched ? 'yes' : 'no'}>"{name}"</span>
  ) : null;

  if (!isContainer) {
    return (
      <div className="jv-row" data-matched={matched ? 'yes' : 'no'}>
        <span className="jv-indent" style={{ width: depth * 16 }} />
        <span className="jv-spacer" />
        {keyEl}{name !== undefined && <span className="jv-colon">:</span>}
        <JsonLeaf value={value} type={t} highlight={search && String(value).toLowerCase().includes(search.toLowerCase())} />
      </div>
    );
  }

  const entries = t === 'array'
    ? value.map((v, i) => [i, v])
    : Object.entries(value);

  // Pagination
  const [shown, setShown] = useState(PAGE_SIZE);
  const visible = entries.slice(0, shown);

  return (
    <>
      <div className="jv-row jv-container" onClick={toggle}>
        <span className="jv-indent" style={{ width: depth * 16 }} />
        <span className={`jv-toggle ${isOpen ? 'is-open' : ''}`}>
          <Icon name="chevron_right" />
        </span>
        {keyEl}{name !== undefined && <span className="jv-colon">:</span>}
        <span className="jv-bracket">{t === 'array' ? '[' : '{'}</span>
        {!isOpen && (
          <>
            <span className="jv-stats">{stats.size} {stats.size === 1 ? 'item' : 'items'}</span>
            <span className="jv-bracket">{t === 'array' ? ']' : '}'}</span>
          </>
        )}
      </div>
      {isOpen && visible.map(([k, v]) => (
        <JsonNode
          key={k}
          name={k}
          value={v}
          depth={depth + 1}
          expanded={expanded}
          setExpanded={setExpanded}
          path={path + (t === 'array' ? `[${k}]` : `.${k}`)}
          search={search}
          matchSet={matchSet}
          defaultOpen={defaultOpen}
        />
      ))}
      {isOpen && shown < entries.length && (
        <div className="jv-row">
          <span className="jv-indent" style={{ width: (depth + 1) * 16 }} />
          <button className="jv-show-more" onClick={(e) => { e.stopPropagation(); setShown(s => s + PAGE_SIZE); }}>
            Show {Math.min(PAGE_SIZE, entries.length - shown)} more · {entries.length - shown} hidden
          </button>
        </div>
      )}
      {isOpen && (
        <div className="jv-row">
          <span className="jv-indent" style={{ width: depth * 16 }} />
          <span className="jv-spacer" />
          <span className="jv-bracket close">{t === 'array' ? ']' : '}'}</span>
        </div>
      )}
    </>
  );
}

function JsonLeaf({ value, type, highlight }) {
  if (type === 'null')      return <span className="jv-leaf null">null</span>;
  if (type === 'boolean')   return <span className="jv-leaf bool">{String(value)}</span>;
  if (type === 'number')    return <span className="jv-leaf num">{value}</span>;
  if (type === 'string')    return <span className={`jv-leaf str ${highlight ? 'jv-hl' : ''}`}>"{value}"</span>;
  return <span className="jv-leaf">{String(value)}</span>;
}

// ============================================================
// Build search match-set: path → has match
// ============================================================
function buildMatchSet(root, query) {
  if (!query) return null;
  const q = query.toLowerCase();
  const set = new Set();
  function walk(v, path) {
    if (v && typeof v === 'object') {
      const entries = Array.isArray(v) ? v.map((x,i) => [i, x]) : Object.entries(v);
      let anyChild = false;
      for (const [k, child] of entries) {
        const childPath = path + (Array.isArray(v) ? `[${k}]` : `.${k}`);
        if (String(k).toLowerCase().includes(q)) { set.add(childPath); anyChild = true; }
        if (walk(child, childPath)) { anyChild = true; }
      }
      if (anyChild) set.add(path);
      return anyChild;
    } else {
      if (String(v).toLowerCase().includes(q)) {
        set.add(path);
        return true;
      }
    }
    return false;
  }
  walk(root, '$');
  return set;
}

// ============================================================
// Document size summary
// ============================================================
function describeSize(text, parsed) {
  const bytes = new Blob([text || '']).size;
  const kb = bytes / 1024;
  const sz = bytes < 1024 ? `${bytes} B` : kb < 1024 ? `${kb.toFixed(1)} KB` : `${(kb/1024).toFixed(2)} MB`;
  let nodes = 0;
  function count(v) { nodes++; if (v && typeof v === 'object') for (const c of Object.values(v)) count(c); }
  if (parsed && parsed !== undefined) count(parsed);
  return { sz, nodes };
}

// ============================================================
// Main JSON Tool
// ============================================================
function JsonTool() {
  const docs = useLiveQuery('jsonDocs', { sort: ['updatedAt', 'desc'] });
  const [selectedId, setSelectedId] = useState(null);
  const [text, setText] = useState('');
  const [title, setTitle] = useState('Untitled');
  const [view, setView] = useState('tree'); // tree | raw
  const [search, setSearch] = useState('');
  const [pathQuery, setPathQuery] = useState('');
  const [expanded, setExpanded] = useState({});
  const [parseError, setParseError] = useState(null);
  const [sidebarCollapsed, toggleSidebar] = useToolSidebar('json');

  // Receive payload from another tool
  useReceiveFrom('json', (payload) => {
    if (payload?.selectId) {
      setSelectedId(payload.selectId);
      return;
    }
    if (payload?.text) {
      const id = db.uid('jsonDocs_');
      db.jsonDocs.put({ id, title: payload.title || 'From ' + (payload.from || 'send'), body: payload.text });
      setSelectedId(id);
      setText(payload.text);
      setTitle(payload.title || 'From send');
    }
  });

  useEffect(() => {
    if (!selectedId && docs && docs.length > 0) {
      setSelectedId(docs[0].id);
    }
  }, [docs, selectedId]);

  const lastLoadedDocIdRef = useRef(null);
  useEffect(() => {
    if (lastLoadedDocIdRef.current === selectedId) return;
    const doc = (docs || []).find(d => d.id === selectedId);
    if (doc) {
      setText(doc.body || '');
      setTitle(doc.title || 'Untitled');
      lastLoadedDocIdRef.current = selectedId;
    } else if (selectedId === null) {
      lastLoadedDocIdRef.current = null;
    }
  }, [selectedId, docs]);

  // Debounced parse
  const [parsed, setParsed] = useState(null);
  useEffect(() => {
    const id = setTimeout(() => {
      const r = tryParse(text);
      if (r.ok) { setParsed(r.value); setParseError(null); }
      else { setParseError(r); }
    }, 200);
    return () => clearTimeout(id);
  }, [text]);

  // For large docs, default to depth 1 to keep it snappy
  const sizeInfo = useMemo(() => describeSize(text, parsed), [text, parsed]);
  const defaultOpen = sizeInfo.nodes > 5000 ? 1 : sizeInfo.nodes > 500 ? 2 : 3;

  // Path query result
  const pathResult = useMemo(() => {
    if (!parsed) return null;
    if (!pathQuery.trim()) return null;
    try {
      return evalPath(parsed, pathQuery);
    } catch (e) {
      return { error: e.message };
    }
  }, [parsed, pathQuery]);

  const matchSet = useMemo(() => parsed && search ? buildMatchSet(parsed, search) : null, [parsed, search]);

  // When searching, auto-expand matched paths
  useEffect(() => {
    if (!matchSet) return;
    const exp = {};
    for (const p of matchSet) exp[p] = true;
    setExpanded(prev => ({ ...prev, ...exp }));
  }, [matchSet]);

  const newDoc = () => {
    const id = db.uid('jsonDocs_');
    db.jsonDocs.put({ id, title: 'Untitled', body: '{\n  \n}' });
    setSelectedId(id);
  };

  const fileInputRef = useRef(null);
  const openFilePicker = () => fileInputRef.current?.click();
  const handleFileUpload = async (e) => {
    const files = Array.from(e.target.files || []);
    if (!files.length) return;
    let firstId = null;
    for (const f of files) {
      if (f.size > 50 * 1024 * 1024) {
        await window.lhDialog?.confirm({
          title: `"${f.name}" is too large`,
          message: 'Files larger than 50 MB are skipped to keep the browser responsive.',
          confirmLabel: 'OK',
          icon: 'warning',
        });
        continue;
      }
      try {
        const text = await f.text();
        const id = db.uid('jsonDocs_');
        await db.jsonDocs.put({
          id,
          title: f.name.replace(/\.json$/i, '') || 'Untitled',
          body: text,
        });
        if (!firstId) firstId = id;
      } catch (err) {
        await window.lhDialog?.confirm({
          title: `Could not read "${f.name}"`,
          message: err.message || 'Unknown error.',
          confirmLabel: 'OK',
          icon: 'error',
        });
      }
    }
    if (firstId) setSelectedId(firstId);
    e.target.value = ''; // allow re-uploading the same file
  };

  // Drag-drop a JSON file onto the editor pane
  const onPaneDragOver = (e) => {
    if ([...(e.dataTransfer?.types || [])].includes('Files')) {
      e.preventDefault();
      e.currentTarget.classList.add('json-drop-active');
    }
  };
  const onPaneDragLeave = (e) => e.currentTarget.classList.remove('json-drop-active');
  const onPaneDrop = async (e) => {
    e.preventDefault();
    e.currentTarget.classList.remove('json-drop-active');
    const files = Array.from(e.dataTransfer?.files || []);
    if (files.length) handleFileUpload({ target: { files, value: '' } });
  };

  const saveDoc = () => {
    if (!selectedId) {
      const id = db.uid('jsonDocs_');
      db.jsonDocs.put({ id, title, body: text });
      setSelectedId(id);
    } else {
      const doc = (docs || []).find(d => d.id === selectedId);
      db.jsonDocs.put({ ...(doc || {}), id: selectedId, title, body: text });
    }
  };

  const deleteDoc = async () => {
    if (!selectedId) return;
    const doc = (docs || []).find(d => d.id === selectedId);
    const ok = await window.lhDialog.confirm({
      title: 'Delete this document?',
      message: `"${doc?.title || 'Untitled'}" will be permanently removed.`,
      confirmLabel: 'Delete',
      danger: true,
      icon: 'delete_forever',
    });
    if (!ok) return;
    db.jsonDocs.delete(selectedId);
    setSelectedId(null);
    setText(''); setTitle('Untitled');
  };

  const format = () => {
    const r = tryParse(text);
    if (r.ok && r.value !== null) setText(JSON.stringify(r.value, null, 2));
  };
  const minify = () => {
    const r = tryParse(text);
    if (r.ok && r.value !== null) setText(JSON.stringify(r.value));
  };

  const expandAll = () => {
    if (!parsed) return;
    const exp = {};
    function walk(v, path) {
      exp[path] = true;
      if (v && typeof v === 'object') {
        const entries = Array.isArray(v) ? v.map((x,i) => [i,x]) : Object.entries(v);
        for (const [k, c] of entries) walk(c, path + (Array.isArray(v) ? `[${k}]` : `.${k}`));
      }
    }
    walk(parsed, '$');
    setExpanded(exp);
  };
  const collapseAll = () => setExpanded({});

  const copyToClipboard = () => {
    navigator.clipboard?.writeText(text);
  };

  return (
    <div className="tool">
      <div className={`tool-twopane json-twopane ${sidebarCollapsed ? 'is-list-collapsed' : ''}`}>
        {/* List of saved docs */}
        <div className="tool-list">
          <div className="tool-list-collapsed-rail" onClick={toggleSidebar} title="Expand sidebar">
            <Icon name="chevron_right" />
          </div>
          <div className="tool-list-head">
            <h2>Documents</h2>
            <span className="count">{(docs || []).length}</span>
            <IconBtn name="upload_file" title="Upload .json file(s)" onClick={openFilePicker} />
            <IconBtn name="add" title="New document" onClick={newDoc} />
            <ToolSidebarToggle collapsed={sidebarCollapsed} onToggle={toggleSidebar} />
            <input
              ref={fileInputRef}
              type="file"
              accept=".json,application/json,text/plain"
              multiple
              style={{ display: 'none' }}
              onChange={handleFileUpload}
            />
          </div>

          <div className="tool-list-scroll">
            {(docs || []).length === 0 ? (
              <div className="lh-empty" style={{ padding: '40px 16px' }}>
                <Icon name="data_object" />
                <div className="title">No saved JSON yet</div>
                <div className="sub">Press <kbd>+</kbd> or paste below.</div>
              </div>
            ) : (docs || []).map(d => (
              <button
                key={d.id}
                className={`json-row ${selectedId === d.id ? 'is-selected' : ''}`}
                onClick={() => setSelectedId(d.id)}
              >
                <div className="json-row-title">{d.title || 'Untitled'}</div>
                <div className="json-row-meta">
                  <span>{relTime(d.updatedAt)}</span>
                  <span>·</span>
                  <span>{(d.body || '').length} chars</span>
                </div>
                <PinButton type="json" id={d.id} className="row-pin" />
              </button>
            ))}
          </div>
        </div>

        {/* Detail */}
        <div
          className="tool-detail"
          onDragOver={onPaneDragOver}
          onDragLeave={onPaneDragLeave}
          onDrop={onPaneDrop}
        >
          <div className="tool-detail-head">
            <input
              className="json-title-input"
              value={title}
              onChange={e => setTitle(e.target.value)}
              onBlur={saveDoc}
            />
            <div style={{ flex: 1 }} />
            <div className="lh-segmented">
              <button className={view === 'tree' ? 'is-active' : ''} onClick={() => setView('tree')}>
                <Icon name="account_tree" /> Tree
              </button>
              <button className={view === 'raw' ? 'is-active' : ''} onClick={() => setView('raw')}>
                <Icon name="code" /> Raw
              </button>
            </div>
            <button className="lh-btn" onClick={format} title="Format with 2-space indent"><Icon name="auto_fix_high" />Format</button>
            <button className="lh-btn" onClick={minify} title="Minify"><Icon name="compress" />Minify</button>
            <button className="lh-btn" onClick={copyToClipboard}><Icon name="content_copy" />Copy</button>
            <SendToButton payload={{ text, title, from: 'json' }} compact />
            <IconBtn name="save" title="Save" onClick={saveDoc} />
            {selectedId && <IconBtn name="delete" tone="danger" title="Delete" onClick={deleteDoc} />}
          </div>

          {/* Status bar */}
          <div className="json-status">
            <span className={`json-status-pill ${parseError ? 'err' : 'ok'}`}>
              <Icon name={parseError ? 'error' : 'check_circle'} />
              {parseError ? `Error: line ${parseError.line || '?'}, col ${parseError.col || '?'}` : 'Valid JSON'}
            </span>
            <span className="json-status-meta">
              {sizeInfo.sz} · {sizeInfo.nodes.toLocaleString()} nodes
            </span>
            <div style={{ flex: 1 }} />
            {view === 'tree' && (
              <>
                <button className="lh-chip" onClick={expandAll}>Expand all</button>
                <button className="lh-chip" onClick={collapseAll}>Collapse all</button>
              </>
            )}
          </div>

          <div className="json-search-bar">
            <div className="json-search-wrap">
              <Icon name="search" />
              <input
                placeholder="Search keys & values…"
                value={search}
                onChange={e => setSearch(e.target.value)}
              />
              {search && <IconBtn name="close" onClick={() => setSearch('')} />}
            </div>
            <div className="json-path-wrap">
              <Icon name="route" />
              <input
                placeholder="JSONPath: $.items[*].sku"
                value={pathQuery}
                onChange={e => setPathQuery(e.target.value)}
              />
              {pathQuery && <IconBtn name="close" onClick={() => setPathQuery('')} />}
            </div>
          </div>

          {parseError && (
            <div className="json-error-banner">
              <Icon name="error" />
              <span>{parseError.error}</span>
              {parseError.line && <span className="loc">at line {parseError.line}, col {parseError.col}</span>}
            </div>
          )}

          {pathResult && (
            <div className="json-path-result">
              <div className="json-path-head">
                <Icon name="route" />
                <span>JSONPath result</span>
                {pathResult.error
                  ? <span className="err">{pathResult.error}</span>
                  : <span className="count">{pathResult.length} match{pathResult.length === 1 ? '' : 'es'}</span>}
              </div>
              {!pathResult.error && pathResult.length > 0 && (
                <pre className="json-path-pre">
                  {JSON.stringify(pathResult.length === 1 ? pathResult[0] : pathResult, null, 2)}
                </pre>
              )}
            </div>
          )}

          <div className="tool-detail-body no-pad">
            {view === 'tree' ? (
              <div className="json-tree">
                {parsed === null && !parseError ? (
                  <div className="lh-empty">
                    <Icon name="data_object" />
                    <div className="title">Empty document</div>
                    <div className="sub">Switch to Raw view and paste JSON.</div>
                  </div>
                ) : parseError ? (
                  <div className="lh-empty">
                    <Icon name="error" />
                    <div className="title">Cannot parse</div>
                    <div className="sub">Fix the error in Raw view and try again.</div>
                  </div>
                ) : parsed === undefined ? null : (
                  <JsonNode
                    value={parsed}
                    depth={0}
                    expanded={expanded}
                    setExpanded={setExpanded}
                    path="$"
                    search={search}
                    matchSet={matchSet}
                    defaultOpen={defaultOpen}
                  />
                )}
              </div>
            ) : (
              <textarea
                className="json-raw"
                value={text}
                spellCheck={false}
                onChange={e => setText(e.target.value)}
                placeholder='{ "paste": "your JSON here" }'
                onBlur={saveDoc}
              />
            )}
          </div>
        </div>
      </div>
    </div>
  );
}

window.JsonTool = JsonTool;
