// JSON Formatter / Validator — pretty-print, minify, validate, tree view.

window.TOOL_HANDLERS['json-formatter'] = function JsonFormatterTool() {
  const SAMPLE = JSON.stringify({
    name: "MiniMagics",
    version: 2.5,
    open_source: true,
    description: "Privacy-first browser tools",
    categories: ["PDF", "Image", "Converter", "File"],
    stats: { tools: 52, users: null, rating: 4.9 },
    features: [
      { id: 1, title: "No uploads", enabled: true },
      { id: 2, title: "Dark mode", enabled: false }
    ]
  }, null, 2);

  const [raw, setRaw] = React.useState(SAMPLE);
  const [parsed, setParsed] = React.useState(null);
  const [error, setError] = React.useState(null);
  const [viewMode, setViewMode] = React.useState('formatted'); // 'formatted' | 'tree'
  const [collapsed, setCollapsed] = React.useState({});
  const [copied, setCopied] = React.useState(false);
  const debounceRef = React.useRef(null);
  const fileRef = React.useRef(null);

  // ---- validation with debounce ----
  const validate = React.useCallback((text) => {
    if (!text.trim()) { setParsed(null); setError(null); return; }
    try {
      const obj = JSON.parse(text);
      setParsed(obj);
      setError(null);
    } catch (e) {
      setParsed(null);
      // try to extract position from message
      const posMatch = e.message.match(/position\s+(\d+)/i);
      let line = null, col = null;
      if (posMatch) {
        const pos = parseInt(posMatch[1], 10);
        const before = text.slice(0, pos);
        line = (before.match(/\n/g) || []).length + 1;
        col = pos - before.lastIndexOf('\n');
      }
      setError({ message: e.message, line, col });
    }
  }, []);

  React.useEffect(() => {
    clearTimeout(debounceRef.current);
    debounceRef.current = setTimeout(() => validate(raw), 250);
    return () => clearTimeout(debounceRef.current);
  }, [raw, validate]);

  // initial validate
  React.useEffect(() => { validate(raw); }, []);

  // ---- actions ----
  const handleFormat = () => {
    if (!parsed) return;
    const formatted = JSON.stringify(parsed, null, 2);
    setRaw(formatted);
  };

  const handleMinify = () => {
    if (!parsed) return;
    setRaw(JSON.stringify(parsed));
  };

  const handleCopy = () => {
    const text = parsed ? JSON.stringify(parsed, null, 2) : raw;
    navigator.clipboard.writeText(text).then(() => {
      setCopied(true);
      setTimeout(() => setCopied(false), 1500);
    });
  };

  const handleClear = () => {
    setRaw('');
    setParsed(null);
    setError(null);
    setCollapsed({});
  };

  const handleFile = (e) => {
    const file = e.target.files?.[0];
    if (!file) return;
    const reader = new FileReader();
    reader.onload = () => { setRaw(reader.result); setCollapsed({}); };
    reader.readAsText(file);
    e.target.value = '';
  };

  // ---- statistics ----
  const stats = React.useMemo(() => {
    if (!parsed) return null;
    let keyCount = 0;
    let maxDepth = 0;
    const walk = (val, depth) => {
      if (depth > maxDepth) maxDepth = depth;
      if (val && typeof val === 'object' && !Array.isArray(val)) {
        const keys = Object.keys(val);
        keyCount += keys.length;
        keys.forEach((k) => walk(val[k], depth + 1));
      } else if (Array.isArray(val)) {
        val.forEach((v) => walk(v, depth + 1));
      }
    };
    walk(parsed, 0);
    const rootType = Array.isArray(parsed) ? 'array' : typeof parsed === 'object' && parsed !== null ? 'object' : typeof parsed;
    return { keyCount, maxDepth, rootType };
  }, [parsed]);

  // ---- syntax highlighting ----
  const highlightJson = React.useCallback((text) => {
    // tokenize JSON string for syntax coloring
    const escaped = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
    return escaped.replace(
      /("(?:\\.|[^"\\])*")\s*:/g,  // keys
      '<span style="color:var(--id-brand-blue)">$1</span>:'
    ).replace(
      /:\s*("(?:\\.|[^"\\])*")/g,  // string values
      ': <span style="color:#16a34a">$1</span>'
    ).replace(
      // string values inside arrays (not preceded by colon on same match)
      /(?<=[\[,]\s*)("(?:\\.|[^"\\])*")/g,
      '<span style="color:#16a34a">$1</span>'
    ).replace(
      /\b(-?\d+\.?\d*(?:[eE][+-]?\d+)?)\b/g,
      '<span style="color:#ea580c">$1</span>'
    ).replace(
      /\b(true|false)\b/g,
      '<span style="color:#7c3aed">$1</span>'
    ).replace(
      /\bnull\b/g,
      '<span style="color:var(--id-text-muted)">null</span>'
    );
  }, []);

  // ---- tree view ----
  const toggleCollapse = React.useCallback((path) => {
    setCollapsed((prev) => ({ ...prev, [path]: !prev[path] }));
  }, []);

  // ---- render ----
  const formattedHtml = React.useMemo(() => {
    if (!parsed) return '';
    return highlightJson(JSON.stringify(parsed, null, 2));
  }, [parsed, highlightJson]);

  return (
    <div className="mini-tool">
      {/* Input area */}
      <div style={{ position: 'relative' }}>
        <label className="mini-label">JSON Input</label>
        <textarea
          className="mini-input mini-textarea"
          style={{
            minHeight: 200,
            fontFamily: 'var(--id-font-mono)',
            fontSize: 12,
            lineHeight: 1.6,
            tabSize: 2,
            whiteSpace: 'pre',
            resize: 'vertical'
          }}
          value={raw}
          onChange={(e) => setRaw(e.target.value)}
          placeholder="Paste or type JSON here..."
          spellCheck={false}
        />
      </div>

      {/* Validation badge */}
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 10, flexWrap: 'wrap' }}>
        {raw.trim() && !error && parsed !== null && (
          <span style={{
            display: 'inline-flex', alignItems: 'center', gap: 4,
            background: '#dcfce7', color: '#16a34a',
            padding: '3px 10px', borderRadius: 6, fontSize: 12, fontWeight: 600
          }}>
            <window.Icon name="check" size={13} /> Valid JSON
          </span>
        )}
        {error && (
          <span style={{
            display: 'inline-flex', alignItems: 'center', gap: 4,
            background: '#fee2e2', color: '#dc2626',
            padding: '3px 10px', borderRadius: 6, fontSize: 12, fontWeight: 600,
            maxWidth: '100%', wordBreak: 'break-word'
          }}>
            <window.Icon name="x" size={13} />
            {error.message}
            {error.line && <span style={{ opacity: 0.7 }}> (line {error.line}, col {error.col})</span>}
          </span>
        )}
      </div>

      {/* Action buttons */}
      <div className="cmp-actions" style={{ marginTop: 14, justifyContent: 'flex-start', flexWrap: 'wrap' }}>
        <button className="btn btn-primary" onClick={handleFormat} disabled={!parsed}>
          <window.Icon name="code" size={15} /> Format
        </button>
        <button className="btn btn-secondary" onClick={handleMinify} disabled={!parsed}>
          <window.Icon name="code" size={15} /> Minify
        </button>
        <button className="btn btn-secondary" onClick={() => validate(raw)}>
          <window.Icon name="check" size={15} /> Validate
        </button>
        <button className="btn btn-secondary" onClick={handleCopy}>
          <window.Icon name="doc" size={15} /> {copied ? 'Copied!' : 'Copy'}
        </button>
        <button className="btn btn-secondary" onClick={handleClear}>
          <window.Icon name="x" size={15} /> Clear
        </button>
        <button className="btn btn-secondary" onClick={() => fileRef.current?.click()} style={{ marginLeft: 'auto' }}>
          <window.Icon name="upload" size={15} /> Load file
        </button>
        <input ref={fileRef} type="file" accept=".json,application/json,.txt" hidden onChange={handleFile} />
      </div>

      {/* Statistics */}
      {stats && (
        <div className="cmp-stats" style={{ marginTop: 16 }}>
          <div className="cmp-stat">
            <div className="sv">{stats.rootType}</div>
            <div className="sl">Root type</div>
          </div>
          <div className="cmp-stat">
            <div className="sv">{stats.keyCount}</div>
            <div className="sl">Keys</div>
          </div>
          <div className="cmp-stat">
            <div className="sv">{stats.maxDepth}</div>
            <div className="sl">Depth</div>
          </div>
          <div className="cmp-stat">
            <div className="sv">{window.fmtBytes(raw.length)}</div>
            <div className="sl">Size</div>
          </div>
        </div>
      )}

      {/* Output area */}
      {parsed !== null && (
        <>
          {/* View mode pills */}
          <div style={{ display: 'flex', gap: 6, marginTop: 20, marginBottom: 10 }}>
            <button
              className={'filter-pill' + (viewMode === 'formatted' ? ' active' : '')}
              onClick={() => setViewMode('formatted')}
            >
              <window.Icon name="code" size={12} /> Formatted
            </button>
            <button
              className={'filter-pill' + (viewMode === 'tree' ? ' active' : '')}
              onClick={() => setViewMode('tree')}
            >
              <window.Icon name="grid" size={12} /> Tree view
            </button>
          </div>

          {/* Formatted view */}
          {viewMode === 'formatted' && (
            <pre
              style={{
                fontFamily: 'var(--id-font-mono)',
                fontSize: 12,
                lineHeight: 1.6,
                background: 'var(--id-surface-sunken)',
                color: 'var(--id-text)',
                border: '1px solid var(--id-border)',
                borderRadius: 10,
                padding: 16,
                overflow: 'auto',
                maxHeight: 500,
                margin: 0,
                whiteSpace: 'pre-wrap',
                wordBreak: 'break-word'
              }}
              dangerouslySetInnerHTML={{ __html: formattedHtml }}
            />
          )}

          {/* Tree view */}
          {viewMode === 'tree' && (
            <div
              style={{
                fontFamily: 'var(--id-font-mono)',
                fontSize: 12,
                lineHeight: 1.7,
                background: 'var(--id-surface-sunken)',
                color: 'var(--id-text)',
                border: '1px solid var(--id-border)',
                borderRadius: 10,
                padding: '12px 8px',
                overflow: 'auto',
                maxHeight: 500
              }}
            >
              <_JfTreeNode
                keyName={null}
                value={parsed}
                path="$"
                isLast={true}
                collapsed={collapsed}
                toggleCollapse={toggleCollapse}
              />
            </div>
          )}
        </>
      )}
    </div>
  );
};

// Standalone TreeNode component — defined outside the tool handler so React
// can reconcile it stably without re-creating on every collapsed state change.
function _JfTreeNode({ keyName, value, path, isLast, collapsed, toggleCollapse }) {
  const isObj = value !== null && typeof value === 'object' && !Array.isArray(value);
  const isArr = Array.isArray(value);
  const isCollapsible = isObj || isArr;
  const isHidden = collapsed[path];
  const comma = isLast ? '' : ',';
  const keySpan = keyName !== null
    ? <span style={{ color: 'var(--id-brand-blue)' }}>"{keyName}"</span> : null;
  const colon = keyName !== null ? ': ' : '';

  if (!isCollapsible) {
    let valSpan;
    if (typeof value === 'string') valSpan = <span style={{ color: '#16a34a' }}>"{value}"</span>;
    else if (typeof value === 'number') valSpan = <span style={{ color: '#ea580c' }}>{String(value)}</span>;
    else if (typeof value === 'boolean') valSpan = <span style={{ color: '#7c3aed' }}>{String(value)}</span>;
    else valSpan = <span style={{ color: 'var(--id-text-muted)' }}>null</span>;
    return <div style={{ paddingLeft: 20 }}>{keySpan}{colon}{valSpan}{comma}</div>;
  }

  const entries = isArr ? value.map((v, i) => [i, v]) : Object.entries(value);
  const open = isArr ? '[' : '{';
  const close = isArr ? ']' : '}';
  const count = entries.length;
  const summary = isArr ? `${count} item${count !== 1 ? 's' : ''}` : `${count} key${count !== 1 ? 's' : ''}`;

  return (
    <div style={{ paddingLeft: 20 }}>
      <span onClick={() => toggleCollapse(path)} style={{ cursor: 'pointer', userSelect: 'none' }}>
        <span style={{ display: 'inline-block', width: 14, textAlign: 'center', color: 'var(--id-text-muted)', fontSize: 10, fontFamily: 'var(--id-font-mono)' }}>
          {isHidden ? '\u25B6' : '\u25BC'}
        </span>
        {keySpan}{colon}
        <span style={{ color: 'var(--id-text)' }}>{open}</span>
        {isHidden && <span style={{ color: 'var(--id-text-muted)', fontSize: 11, fontStyle: 'italic', margin: '0 4px' }}>{summary}</span>}
        {isHidden && <span style={{ color: 'var(--id-text)' }}>{close}{comma}</span>}
      </span>
      {!isHidden && (
        <>
          {entries.map(([k, v], i) => (
            <_JfTreeNode key={String(k)} keyName={isArr ? null : String(k)} value={v}
              path={path + '.' + k} isLast={i === entries.length - 1}
              collapsed={collapsed} toggleCollapse={toggleCollapse} />
          ))}
          <div style={{ paddingLeft: 20 }}><span style={{ color: 'var(--id-text)' }}>{close}{comma}</span></div>
        </>
      )}
    </div>
  );
}
window._JfTreeNode = _JfTreeNode;
