class ClaudeRenderer {
  constructor(outputEl) {
    this.outputEl = outputEl;
    this._lastSignature = null;
    this._onAction = null;
    this._blockEls = null;
    this._blockSigs = null;

    this.statusBarEl = document.createElement('div');
    this.statusBarEl.className = 'cc-status-bar';
  }

  setOnAction(fn) {
    this._onAction = fn;
  }

  _stripAnsi(s) {
    return s.replace(/\x1b(?:\[[0-9;?]*[A-Za-z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|.)/g, '');
  }

  _dedentRunaway(line) {
    // Claude Code's TUI shoves some lines far to the right (often right-aligned to
    // the pane edge) when the cmux pane is very wide (e.g. ~185 cols); read-screen
    // captures the literal leading spaces, so the parsed reading view renders the
    // text pushed off to the right. Prose/output is never legitimately indented this
    // far (lists/quotes stay ≤ ~12), so collapse runaway leading whitespace to the
    // left. Code / tables / diffs are rendered by other paths and never come here.
    return /^[ \t]{16,}\S/.test(line) ? line.replace(/^[ \t]+/, '') : line;
  }

  _addCopyButton(container, text) {
    container.classList.add('cc-copy-wrap');
    const btn = document.createElement('button');
    btn.className = 'cc-copy-btn';
    btn.innerHTML = '&#x2398;';
    btn.title = 'Copy';
    btn.addEventListener('click', (e) => {
      e.stopPropagation();
      navigator.clipboard.writeText(text).then(() => {
        btn.classList.add('copied');
        btn.textContent = '\u2713';
        setTimeout(() => {
          btn.classList.remove('copied');
          btn.innerHTML = '&#x2398;';
        }, 1500);
      });
    });
    container.appendChild(btn);
  }

  render(doc) {
    const sig = this._signature(doc.blocks);
    if (sig !== this._lastSignature) {
      this._renderBlocks(doc.blocks);
      this._lastSignature = sig;
    }
    this._renderStatusBar(doc.statusBar);
  }

  clear() {
    this.outputEl.innerHTML = '';
    if (this.statusBarEl.parentNode) this.statusBarEl.parentNode.removeChild(this.statusBarEl);
    this._lastSignature = null;
    this._blockEls = null;
    this._blockSigs = null;
  }

  _blockSig(block) {
    // Hash the FULL block content, not just the first line. With a first-line-only
    // signature, a block whose inner lines change while type/count/first-line stay
    // the same (very common while streaming: tool output, multi-line text, a
    // selection whose focused option moves) keeps its stale DOM — and because this
    // feeds the top-level render() signature gate, a content-only change skips ALL
    // rendering, freezing the view ("parsed, then not"). Hashing fixes both layers.
    return `${block.type}:${block.lines.length}:${this._hash(block.lines.join(''))}`;
  }

  _hash(s) {
    let h = 5381;
    for (let i = 0; i < s.length; i++) h = (((h << 5) + h) ^ s.charCodeAt(i)) | 0;
    return h;
  }

  _signature(blocks) {
    return blocks.map(b => this._blockSig(b)).join(',');
  }

  _renderBlocks(blocks) {
    const newSigs = blocks.map(b => this._blockSig(b));

    // Full rebuild on first render or block count change
    if (!this._blockEls || this._blockEls.length !== blocks.length) {
      this._fullRebuild(blocks, newSigs);
      return;
    }

    // Per-block update: only replace DOM nodes that changed
    for (let i = 0; i < blocks.length; i++) {
      if (newSigs[i] === this._blockSigs[i]) continue;

      const newEl = this._renderBlock(blocks[i]);
      const oldEl = this._blockEls[i];

      if (newEl && oldEl && oldEl.parentNode) {
        oldEl.parentNode.replaceChild(newEl, oldEl);
        this._blockEls[i] = newEl;
      } else if (!newEl && !oldEl) {
        // both null, nothing to do
      } else {
        // mismatch — fall back to full rebuild
        this._fullRebuild(blocks, newSigs);
        return;
      }
      this._blockSigs[i] = newSigs[i];
    }
  }

  _fullRebuild(blocks, sigs) {
    const frag = document.createDocumentFragment();
    const els = [];
    for (const block of blocks) {
      const el = this._renderBlock(block);
      els.push(el);
      if (el) frag.appendChild(el);
    }
    this.outputEl.innerHTML = '';
    this.outputEl.appendChild(frag);
    this._blockEls = els;
    this._blockSigs = sigs;
  }

  _renderBlock(block) {
    switch (block.type) {
      case 'prompt':      return this._renderPrompt(block);
      case 'tool-use':    return this._renderToolUse(block);
      case 'tool-result': return this._renderToolResult(block);
      case 'response':    return this._renderResponse(block);
      case 'code':        return this._renderCode(block);
      case 'table':       return this._renderTable(block);
      case 'diff':        return this._renderDiff(block);
      case 'selection':   return this._renderSelection(block);
      case 'thinking':    return this._renderThinking(block);
      case 'text':        return this._renderText(block);
      case 'empty':       return this._renderEmpty();
      default:            return null;
    }
  }

  _esc(text) {
    return text
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;');
  }

  // Pattern-based coloring for a single line (no ANSI codes from server)
  _renderLine(line) {
    const stripped = this._stripAnsi(line);
    let e = this._esc(stripped);
    // Dingbats — spinner/thinking characters (✻ ✳ ✢ ✦ etc.)
    e = e.replace(/([\u2700-\u27BF])/g, '<span style="color:var(--yellow)">$1</span>');
    return e;
  }

  _renderPrompt(block) {
    const div = document.createElement('div');
    div.className = 'cc-prompt';

    const firstLine = block.lines[0];
    const stripped = this._stripAnsi(firstLine);
    const markerMatch = stripped.match(/^(\s*\u276F\s*)/);
    const text = markerMatch ? firstLine.slice(markerMatch[1].length) : firstLine;

    const marker = document.createElement('span');
    marker.className = 'cc-prompt-marker';
    marker.textContent = '❯';

    const textEl = document.createElement('span');
    textEl.className = 'cc-prompt-text';
    textEl.innerHTML = ansiToHtml(text);

    div.appendChild(marker);
    div.appendChild(textEl);

    for (let i = 1; i < block.lines.length; i++) {
      const cont = document.createElement('div');
      cont.className = 'cc-prompt-cont';
      cont.innerHTML = ansiToHtml(block.lines[i]);
      div.appendChild(cont);
    }

    return div;
  }

  _renderToolUse(block) {
    const div = document.createElement('div');
    const firstLine = block.lines[0];
    // Match: ⏺ ToolName(args) or ⏺ ToolName args
    const match = firstLine.match(/[\u23FA\u25CF\u2B24]\s*(\w[\w.]*)?(.*)$/);

    if (!match || !match[1]) {
      // No tool name — this is response text with ⏺ prefix; render as plain text
      div.className = 'cc-text';
      for (const line of block.lines) {
        const lineEl = document.createElement('div');
        lineEl.className = 'cc-line';
        lineEl.innerHTML = ansiToHtml(line.replace(/^\s+/, ''));
        div.appendChild(lineEl);
      }
      return div;
    }

    div.className = 'cc-tool-use';

    const header = document.createElement('div');
    header.className = 'cc-tool-header';

    const icon = document.createElement('span');
    icon.className = 'cc-tool-icon';
    icon.textContent = '⏺';

    const name = document.createElement('span');
    name.className = 'cc-tool-name';
    name.textContent = match[1];

    const args = document.createElement('span');
    args.className = 'cc-tool-args';
    args.textContent = (match[2] || '').trim();

    header.appendChild(icon);
    header.appendChild(name);
    if (match[2] && match[2].trim()) header.appendChild(args);

    div.appendChild(header);

    if (block.lines.length > 1) {
      const body = document.createElement('div');
      body.className = 'cc-tool-body';
      for (let i = 1; i < block.lines.length; i++) {
        const line = document.createElement('div');
        line.className = 'cc-line';
        line.innerHTML = ansiToHtml(this._dedentRunaway(block.lines[i]));
        body.appendChild(line);
      }
      div.appendChild(body);

      const bodyLineCount = block.lines.length - 1;
      if (bodyLineCount > 5) {
        div.classList.add('cc-collapsible');
        const toggle = document.createElement('button');
        toggle.className = 'cc-collapse-toggle';
        toggle.textContent = `Show all (${bodyLineCount} lines)`;
        toggle.addEventListener('click', () => {
          const expanded = div.classList.toggle('cc-expanded');
          toggle.textContent = expanded ? 'Show less' : `Show all (${bodyLineCount} lines)`;
        });
        div.appendChild(toggle);
      }
    }

    return div;
  }

  _renderToolResult(block) {
    const div = document.createElement('div');
    div.className = 'cc-tool-result';

    const scrollable = document.createElement('div');
    scrollable.className = 'cc-scrollable';

    const pre = document.createElement('pre');

    // Strip ╭/╰ border lines; for │ content lines, remove the │ markers
    // and trailing terminal padding so content is immune to terminal width changes
    const contentLines = [];
    for (const line of block.lines) {
      const s = this._stripAnsi(line).trimStart();
      if (/^[│┃║]/.test(s)) {
        let cleaned = line.replace(/^(?:\x1b(?:\[[0-9;?]*[A-Za-z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|.))*[│┃║]\s?/, '');
        cleaned = cleaned.replace(/\s*[│┃║](?:\x1b(?:\[[0-9;?]*[A-Za-z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|.))*\s*$/, '');
        contentLines.push(cleaned.trimEnd());
      }
      // Skip ╭/╰ border lines
    }

    pre.innerHTML = contentLines.map(l => ansiToHtml(l)).join('\n');
    scrollable.appendChild(pre);
    div.appendChild(scrollable);

    // Collapsible for long results
    if (contentLines.length > 8) {
      div.classList.add('cc-collapsible');
      const toggle = document.createElement('button');
      toggle.className = 'cc-collapse-toggle';
      toggle.textContent = `Show all (${contentLines.length} lines)`;
      toggle.addEventListener('click', () => {
        const expanded = div.classList.toggle('cc-expanded');
        toggle.textContent = expanded ? 'Show less' : `Show all (${contentLines.length} lines)`;
      });
      div.appendChild(toggle);
    }

    this._addCopyButton(div, contentLines.map(l => this._stripAnsi(l)).join('\n'));
    return div;
  }

  _renderThinking(block) {
    const div = document.createElement('div');
    div.className = 'cc-thinking';
    div.textContent = this._stripAnsi(block.lines.join('\n'));
    return div;
  }

  _renderResponse(block) {
    const div = document.createElement('div');
    div.className = 'cc-response';
    for (const line of block.lines) {
      const lineEl = document.createElement('div');
      lineEl.className = 'cc-line';
      // Strip ⎿ prefix marker, preserve ANSI colors
      const content = line.replace(/^(\x1b\[[0-9;]*m)*\s*\u23BF\s?/, '');
      lineEl.innerHTML = ansiToHtml(this._dedentRunaway(content));
      div.appendChild(lineEl);
    }
    return div;
  }

  _renderText(block) {
    const div = document.createElement('div');
    div.className = 'cc-text';
    for (const line of block.lines) {
      const lineEl = document.createElement('div');
      lineEl.className = 'cc-line';
      lineEl.innerHTML = ansiToHtml(this._dedentRunaway(line));
      div.appendChild(lineEl);
    }
    return div;
  }

  _renderCode(block) {
    const div = document.createElement('div');
    div.className = 'cc-code';

    const scrollable = document.createElement('div');
    scrollable.className = 'cc-scrollable';

    const pre = document.createElement('pre');
    pre.className = 'cc-code-content';
    pre.innerHTML = block.lines.map(l => ansiToHtml(l)).join('\n');

    scrollable.appendChild(pre);
    div.appendChild(scrollable);
    this._addCopyButton(div, block.lines.map(l => this._stripAnsi(l)).join('\n'));
    return div;
  }

  _renderTable(block) {
    const div = document.createElement('div');
    div.className = 'cc-table-wrap';

    const scrollable = document.createElement('div');
    scrollable.className = 'cc-scrollable';

    const table = document.createElement('table');
    table.className = 'cc-table';

    const thead = document.createElement('thead');
    const tbody = document.createElement('tbody');
    let headerDone = false;

    const isMiddleBorder = s => /^[├╠┣]/.test(s);
    const isBorderLine = s => /^[┌┐└┘├┤┬┴┼╔╗╚╝╠╣╦╩╬┏┓┗┛┣┫┳┻╋]/.test(s);
    const isDataLine = s => /^[│┃║]/.test(s);

    const lines = block.lines;
    for (let i = 0; i < lines.length; i++) {
      const stripped = this._stripAnsi(lines[i]).trimStart();

      if (isBorderLine(stripped)) {
        if (isMiddleBorder(stripped)) headerDone = true;
        continue;
      }

      if (!isDataLine(stripped)) continue;

      // Check if the next border is a middle border (making this row a header)
      let nextIsMiddleBorder = false;
      if (!headerDone) {
        for (let j = i + 1; j < lines.length; j++) {
          const ns = this._stripAnsi(lines[j]).trimStart();
          if (!ns) continue;
          if (isMiddleBorder(ns)) nextIsMiddleBorder = true;
          break;
        }
      }

      const isHeader = !headerDone && nextIsMiddleBorder;
      const cells = stripped.split(/[│┃║]/).slice(1, -1);

      const tr = document.createElement('tr');
      for (const cell of cells) {
        const cellEl = document.createElement(isHeader ? 'th' : 'td');
        cellEl.textContent = cell.trim();
        tr.appendChild(cellEl);
      }

      if (isHeader) {
        thead.appendChild(tr);
      } else {
        tbody.appendChild(tr);
      }
    }

    if (thead.children.length > 0) table.appendChild(thead);
    table.appendChild(tbody);
    scrollable.appendChild(table);
    div.appendChild(scrollable);
    return div;
  }

  _renderDiff(block) {
    const div = document.createElement('div');
    div.className = 'cc-diff';

    const scrollable = document.createElement('div');
    scrollable.className = 'cc-scrollable';

    const content = document.createElement('div');
    content.className = 'cc-diff-content';

    for (const line of block.lines) {
      const span = document.createElement('span');
      span.className = 'cc-diff-line';
      if (/^\+(?!\+)/.test(line))     span.classList.add('cc-added');
      else if (/^-(?!-)/.test(line))  span.classList.add('cc-removed');
      else if (/^@@/.test(line))      span.classList.add('cc-hunk');
      span.innerHTML = ansiToHtml(line);
      content.appendChild(span);
      content.appendChild(document.createTextNode('\n'));
    }

    scrollable.appendChild(content);
    div.appendChild(scrollable);
    this._addCopyButton(div, block.lines.map(l => this._stripAnsi(l)).join('\n'));
    return div;
  }

  _renderSelection(block) {
    const div = document.createElement('div');
    div.className = 'cc-selection-options';

    if (block.options && block.options.length > 0) {
      for (const opt of block.options) {
        const btn = document.createElement('button');
        btn.className = 'cc-option' + (opt.focused ? ' cc-option-focused' : '');
        btn.dataset.key = opt.key;

        const indexEl = document.createElement('span');
        indexEl.className = 'cc-option-index';
        indexEl.textContent = opt.index;

        const labelEl = document.createElement('span');
        labelEl.className = 'cc-option-label';
        labelEl.textContent = opt.label;

        const hint = document.createElement('span');
        hint.className = 'cc-option-hint';
        hint.textContent = `⌨ ${opt.key}`;

        btn.appendChild(indexEl);
        btn.appendChild(labelEl);
        btn.appendChild(hint);
        btn.addEventListener('click', () => {
          if (this._onAction) this._onAction('select', opt.key);
        });
        div.appendChild(btn);
      }
    } else {
      // Fallback: render lines as text
      for (const line of block.lines) {
        const lineEl = document.createElement('div');
        lineEl.className = 'cc-line';
        lineEl.textContent = line;
        div.appendChild(lineEl);
      }
    }

    return div;
  }

  _renderEmpty() {
    const div = document.createElement('div');
    div.className = 'cc-empty';
    return div;
  }

  _renderStatusBar(statusBar) {
    if (!statusBar || !this.statusBarEl) return;

    this.statusBarEl.innerHTML = '';

    const mode = statusBar.mode || 'ask';
    {
      const pill = document.createElement('button');
      pill.className = `cc-mode-pill cc-mode-${mode}`;
      const icons  = { plan: '⏸', ask: '●', bypass: '⏵⏵', accept: '✓' };
      const labels = { plan: 'Plan', ask: 'Ask', bypass: 'Bypass', accept: 'Accept' };
      pill.textContent = `${icons[mode] || '●'} ${labels[mode] || mode}`;
      pill.addEventListener('click', () => {
        if (this._onAction) this._onAction('mode-toggle', mode);
      });
      this.statusBarEl.appendChild(pill);
    }

    if (statusBar.model) {
      const model = document.createElement('span');
      model.className = 'cc-status-model';
      model.textContent = statusBar.model;
      this.statusBarEl.appendChild(model);
    }

    if (statusBar.branch) {
      const branch = document.createElement('span');
      branch.className = 'cc-status-branch';
      branch.textContent = statusBar.branch;
      this.statusBarEl.appendChild(branch);
    }

    if (statusBar.ctxPct != null) {
      const bar = document.createElement('div');
      bar.className = 'cc-context-bar';
      const fill = document.createElement('div');
      fill.className = 'cc-context-fill';
      fill.style.width = `${statusBar.ctxPct}%`;
      if (statusBar.ctxPct < 30) fill.classList.add('cc-ctx-low');
      else if (statusBar.ctxPct < 60) fill.classList.add('cc-ctx-mid');
      bar.appendChild(fill);
      this.statusBarEl.appendChild(bar);
    }

    if (statusBar.stats) {
      const stats = document.createElement('span');
      stats.className = 'cc-status-stats';
      stats.textContent = statusBar.stats;
      this.statusBarEl.appendChild(stats);
    }
  }
}

window.ClaudeRenderer = ClaudeRenderer;
