class ClaudeParser {
  constructor() {
    this._lastLines = null;
    this._lastDoc = null;
  }

  parse(lines) {
    if (lines === this._lastLines) return this._lastDoc;
    this._lastLines = lines;
    this._lastDoc = this._doParse(lines);
    return this._lastDoc;
  }

  _doParse(lines) {
    if (!lines || lines.length === 0) {
      return { blocks: [], statusBar: null };
    }

    const statusBarIndices = this._findStatusBarLines(lines);
    const statusBar = this._mergeStatusBars(lines, statusBarIndices);

    // Content lines: exclude all status bar lines
    const statusSet = new Set(statusBarIndices);
    const contentLines = statusBarIndices.length > 0
      ? lines.filter((_, i) => !statusSet.has(i))
      : lines;

    const blocks = this._parseBlocks(contentLines);

    return { blocks, statusBar };
  }

  _findStatusBarLines(lines) {
    // Collect all status bar line indices in the bottom 5 non-empty lines
    const indices = [];
    let checked = 0;
    for (let i = lines.length - 1; i >= 0 && checked < 5; i--) {
      const line = lines[i];
      if (!line || !line.trim()) continue;
      checked++;
      if (this._tryParseStatusBar(line)) indices.push(i);
    }
    return indices;
  }

  _mergeStatusBars(lines, indices) {
    if (indices.length === 0) return null;
    const parsed = indices.map(i => this._tryParseStatusBar(lines[i])).filter(Boolean);
    const merged = { mode: null, branch: null, model: null, stats: null, ctxPct: null, rawLine: parsed[0].rawLine };
    for (const p of parsed) {
      if (p.mode) merged.mode = p.mode;
      if (p.model) merged.model = p.model;
      if (p.stats) merged.stats = p.stats;
      if (p.ctxPct != null) merged.ctxPct = p.ctxPct;
      if (p.branch) merged.branch = p.branch;
    }
    return merged;
  }

  _tryParseStatusBar(line) {
    if (!line) return null;
    // Strip ANSI then strip remaining C0/C1 control characters for robust matching
    const stripped = this._stripAnsi(line).replace(/[\x00-\x1f\x7f-\x9f]/g, '').trim();
    if (!stripped) return null;

    // Exclude prompt and spinner/thinking lines — not a status bar
    if (/^[\u276F\u2700-\u27BF]/.test(stripped)) return null;

    // New format: "⏵⏵ bypass permissions on (shift+tab to cycle)"
    //             "⏸ plan mode (shift+tab to cycle)"
    //             "⏵ accept edits on (shift+tab to cycle)"
    if (/[\u23F8\u23F5]/.test(stripped) && /shift\+tab/i.test(stripped)) {
      let mode = 'ask';
      if (/\u23F8/.test(stripped) || /plan\s+mode/i.test(stripped)) mode = 'plan';
      else if (/bypass/i.test(stripped)) mode = 'bypass';
      else if (/accept\s+edits/i.test(stripped)) mode = 'accept';
      // A custom statusline often shares this row ("…  Opus 4.8  ctx:96%  ⏵⏵ bypass
      // … shift+tab …"), so pull ctx%/compact% and model from the same line instead
      // of dropping them (the dedicated ctx:/compact branches below never run when
      // the mode glyph short-circuits here).
      const ctxMatch = stripped.match(/ctx:(\d+)%/i);
      const compactMatch = stripped.match(/(\d+)%\s+until\s+auto-compact/i);
      const modelMatch = stripped.match(/(Opus|Sonnet|Haiku)(?:\s+(\d+(?:\.\d+)?))?/i);
      const ctxPct = ctxMatch ? parseInt(ctxMatch[1], 10)
        : (compactMatch ? parseInt(compactMatch[1], 10) : null);
      const stats = ctxMatch ? `${ctxMatch[1]}%`
        : (compactMatch ? `${compactMatch[1]}% context` : null);
      const model = modelMatch
        ? (modelMatch[2] ? `${modelMatch[1]} ${modelMatch[2]}` : modelMatch[1])
        : null;
      return { mode, branch: null, model, stats, ctxPct, rawLine: line };
    }

    // Combined line: "user@host ...  11% until auto-compact" (tmux status + Claude compact %)
    const compactMatch = stripped.match(/(\d+)%\s+until\s+auto-compact/i);
    if (compactMatch) {
      const ctxPct = parseInt(compactMatch[1], 10);
      const modelMatch = stripped.match(/(Opus|Sonnet|Haiku)/i);
      const model = modelMatch ? modelMatch[1] : null;
      return { mode: null, branch: null, model, stats: `${ctxPct}% context`, ctxPct, rawLine: line };
    }

    // Custom statusline: "user@host repo branch  Sonnet 4.6  ctx:60%"
    // ctx: is the definitive signal — model name is optional secondary info
    const ctxMatch = stripped.match(/ctx:(\d+)%/i);
    if (ctxMatch) {
      const ctxPct = parseInt(ctxMatch[1], 10);
      const modelMatch = stripped.match(/(Opus|Sonnet|Haiku)\s+(\d+(?:\.\d+)?)/i);
      const model = modelMatch ? `${modelMatch[1]} ${modelMatch[2]}` : null;
      return { mode: null, branch: null, model, stats: `${ctxPct}%`, ctxPct, rawLine: line };
    }

    // Old format: "⏵ bypass · main · 3.2k tokens"
    if (!stripped.includes('·')) return null;

    const parts = stripped.split('·').map(p => p.trim());
    if (parts.length < 2) return null;

    // Require a leading mode glyph (\u23F8/\u23F5) \u2014 otherwise any prose or question line
    // containing a "\u00B7" middot (e.g. an inline options prompt) gets mis-detected as a
    // status bar and both eaten from the content and leaked into the status pill.
    const firstPart = parts[0];
    let mode = null;
    if (/\u23F8/.test(firstPart)) {
      mode = 'plan';
    } else if (/\u23F5/.test(firstPart)) {
      mode = 'bypass';
    } else {
      return null;
    }

    // Branch: first part with no spaces that looks like a branch name
    let branch = null;
    for (const part of parts) {
      if (part && !part.includes(' ') && /^[\w][\w./_-]*$/.test(part) && part.length < 50) {
        branch = part;
        break;
      }
    }

    const branchIdx = parts.findIndex(p => p === branch);
    const statsParts = parts.filter((_, i) => i !== 0 && i !== branchIdx);
    const stats = statsParts.length > 0 ? statsParts.join(' · ') : null;

    return { mode, branch, model: null, stats, ctxPct: null, rawLine: line };
  }

  _parseBlocks(lines) {
    const blocks = [];

    // Pre-scan for selection ranges near the bottom
    const selectionRanges = this._detectSelectionRanges(lines);
    const selectionLineSet = new Set();
    selectionRanges.forEach(r => {
      for (let i = r.start; i <= r.end; i++) selectionLineSet.add(i);
    });

    let i = 0;
    while (i < lines.length) {
      // Selection block
      if (selectionLineSet.has(i)) {
        const range = selectionRanges.find(r => i === r.start);
        if (range) {
          const selLines = lines.slice(range.start, range.end + 1);
          blocks.push({
            type: 'selection',
            lines: selLines,
            toolName: null,
            options: this._parseOptions(selLines),
          });
          i = range.end + 1;
          continue;
        }
      }

      const line = lines[i];
      const s = this._stripAnsi(line);

      // Empty
      if (/^\s*$/.test(s)) {
        blocks.push({ type: 'empty', lines: [line], toolName: null, options: null });
        i++;
        continue;
      }

      // Table block: lines using box-drawing table characters (│, ┃, ┌, ├, etc.)
      // Must be detected BEFORE _isSeparatorLine because border rows (├──┤) look like separators
      if (this._isTableRowLine(line) || this._isTableBorderLine(line)) {
        const bl = [line]; i++;
        while (i < lines.length) {
          if (this._isTableRowLine(lines[i]) || this._isTableBorderLine(lines[i])) {
            bl.push(lines[i++]);
          } else {
            break;
          }
        }
        // A real markdown table has a border row with column tees (┌─┬─┐ / ├─┼─┤).
        // Decorative boxes (welcome banner, notices, the input frame) use only │
        // side borders / rounded corners and would be mangled into garbage cells by
        // the table renderer — render those as plain monospace text instead.
        if (this._hasTableStructure(bl)) {
          blocks.push({ type: 'table', lines: bl, toolName: null, options: null });
        } else {
          blocks.push({ type: 'text', lines: bl, toolName: null, options: null });
        }
        continue;
      }

      // Skip separator lines (─/━/═ box-drawing horizontal rules used as UI chrome)
      if (this._isSeparatorLine(line)) {
        i++;
        continue;
      }

      // ❯ Prompt (U+276F)
      if (/^\s*\u276F/.test(s)) {
        const bl = [line]; i++;
        while (i < lines.length && !this._isBlockStart(lines[i]) && !/^\s*$/.test(lines[i])) {
          // Stop collecting at separator lines or selection option lines
          if (this._isSeparatorLine(lines[i])) break;
          if (selectionLineSet.has(i)) break;
          bl.push(lines[i++]);
        }
        // Skip empty prompt blocks (input cursor). Strip ANSI first so color codes
        // before ❯ don't prevent the match. Also skip when content is only separator
        // chars — Claude Code renders ❯  ────── on the active input line.
        const firstLineText = this._stripAnsi(bl[0]).replace(/^\s*❯\s*/, '').trim();
        if (!firstLineText || this._isSeparatorLine(firstLineText)) continue;
        blocks.push({ type: 'prompt', lines: bl, toolName: null, options: null });
        continue;
      }

      // ⏺/●/⬤ Tool-use (U+23FA / U+25CF / U+2B24)
      if (/^\s*[\u23FA\u25CF\u2B24]/.test(s)) {
        const toolName = this._extractToolName(line);
        const bl = [line]; i++;
        // Collect indented continuation lines (use stripped version for indent check)
        while (i < lines.length) {
          const cs = this._stripAnsi(lines[i]);
          if (/^ {2,}/.test(cs) && !this._isBlockStart(lines[i])) {
            bl.push(lines[i++]);
          } else break;
        }
        blocks.push({ type: 'tool-use', lines: bl, toolName, options: null });
        continue;
      }

      // ⎿ Response output marker (U+23BF)
      if (/^\s*\u23BF/.test(s)) {
        // Skip tip/hint lines entirely
        const lineStripped = line.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
        if (/Tip:/i.test(lineStripped)) { i++; continue; }
        const bl = [line]; i++;
        while (i < lines.length && /^\s*\u23BF/.test(this._stripAnsi(lines[i]))) {
          bl.push(lines[i++]);
        }
        blocks.push({ type: 'response', lines: bl, toolName: null, options: null });
        continue;
      }

      // ╭ Tool-result box (U+256D)
      if (/^\s*\u256D/.test(s) || s.trimStart().startsWith('╭')) {
        const bl = [line]; i++;
        while (i < lines.length) {
          const l = lines[i];
          bl.push(l); i++;
          // Stop at closing corner ╰ (U+2570) or ╯ (U+256F)
          if (/[\u2570\u256F]/.test(l)) break;
        }
        blocks.push({ type: 'tool-result', lines: bl, toolName: null, options: null });
        continue;
      }

      // Thinking: lines with only spinner/dingbat characters (✻✳✢✦ U+2700-U+27BF)
      if (/^\s*[\u2700-\u27BF]+\s*$/.test(s)) {
        const bl = [line]; i++;
        while (i < lines.length) {
          const ns = this._stripAnsi(lines[i]);
          if (/^\s*[\u2700-\u27BF]+\s*$/.test(ns) || /^\s*$/.test(ns)) {
            bl.push(lines[i++]);
          } else break;
        }
        blocks.push({ type: 'thinking', lines: bl, toolName: null, options: null });
        continue;
      }

      // Diff: +/- lines (only when clearly a diff context)
      if (/^[+-](?![+-])/.test(s) || /^@@/.test(s)) {
        const bl = [line]; i++;
        while (i < lines.length) {
          const cs = this._stripAnsi(lines[i]);
          if (/^[+\- @]/.test(cs) && !this._isBlockStart(lines[i])) {
            bl.push(lines[i++]);
          } else break;
        }
        blocks.push({ type: 'diff', lines: bl, toolName: null, options: null });
        continue;
      }

      // Code: 4+ space indent with code-like content
      if (/^ {4,}/.test(s) && this._isCodeLike(s)) {
        const bl = [line]; i++;
        while (i < lines.length) {
          const cs = this._stripAnsi(lines[i]);
          if (/^ {4,}/.test(cs)) {
            bl.push(lines[i++]);
          } else if (/^\s*$/.test(lines[i]) && i + 1 < lines.length && /^ {4,}/.test(this._stripAnsi(lines[i + 1]))) {
            bl.push(lines[i++]); // include bridging empty line
          } else {
            break;
          }
        }
        // Trim trailing empty lines
        while (bl.length > 0 && /^\s*$/.test(bl[bl.length - 1])) bl.pop();
        if (bl.length > 0) {
          blocks.push({ type: 'code', lines: bl, toolName: null, options: null });
        }
        continue;
      }

      // Text: fallback — collect consecutive non-special lines
      const bl = [line]; i++;
      while (i < lines.length && !this._isBlockStart(lines[i]) && !/^\s*$/.test(lines[i]) && !selectionLineSet.has(i)) {
        bl.push(lines[i++]);
      }
      blocks.push({ type: 'text', lines: bl, toolName: null, options: null });
    }

    return blocks;
  }

  _stripAnsi(line) {
    // Strip ANSI escape sequences, then C0/C1 control chars (Shift-In \x0f etc.)
    // that tmux can inject before prompt characters, breaking block detection.
    // eslint-disable-next-line no-control-regex
    return line
      .replace(/\x1b(?:\[[0-9;?]*[A-Za-z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|.)/g, '')
      .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/g, '');
  }

  _isBlockStart(line) {
    if (!line || /^\s*$/.test(line)) return false;
    const s = this._stripAnsi(line);
    return (
      /^\s*\u276F/.test(s) ||           // ❯ prompt
      /^\s*[\u23FA\u25CF\u2B24]/.test(s) || // ⏺●⬤ tool-use
      /^\s*\u23BF/.test(s) ||           // ⎿ response
      /^\s*\u256D/.test(s) ||           // ╭ tool-result
      s.trimStart().startsWith('╭') ||
      this._isTableRowLine(line) ||     // │ table row
      this._isTableBorderLine(line)     // ┌├┤┼ table border
    );
  }

  _extractToolName(line) {
    const m = line.match(/[\u23FA\u25CF\u2B24]\s*(\w[\w.]*)/);
    return m ? m[1] : null;
  }

  _isTableRowLine(line) {
    const s = this._stripAnsi(line).trimStart();
    return /^[│┃║]/.test(s);
  }

  _isTableBorderLine(line) {
    const s = this._stripAnsi(line).trimStart();
    return /^[┌┐└┘├┤┬┴┼╔╗╚╝╠╣╦╩╬┏┓┗┛┣┫┳┻╋]/.test(s);
  }

  _hasTableStructure(blockLines) {
    // True only when some line is a horizontal border that splits into columns —
    // i.e. a box-drawing border char AND a column tee (┬ ┼ ┴ + heavy/double
    // variants). Distinguishes a data table from a single-column decorative box.
    for (const l of blockLines) {
      const s = this._stripAnsi(l).trimStart();
      if (/^[┌┐└┘├┤┬┴┼┏┓┗┛┣┫┳┻╋╔╗╚╝╠╣╦╩╬]/.test(s) && /[┬┼┴╦╬╩┳╋┻]/.test(s)) {
        return true;
      }
    }
    return false;
  }

  _isSeparatorLine(line) {
    const stripped = this._stripAnsi(line).trim();
    if (stripped.length < 3) return false;
    // Must consist mostly of horizontal box-drawing characters
    const sepChars = (stripped.match(/[\u2500\u2501\u2550\u254C\u254D\u2574\u2576\u2578\u257A\u2015\u2014]/g) || []).length;
    if (sepChars / stripped.length > 0.4) return true;
    // Also catch lines that are a single character repeated (any char) — e.g. ─────── or ────
    const uniqueChars = new Set(stripped.replace(/\s/g, '')).size;
    return uniqueChars === 1 && stripped.length >= 3 && /[^\w\s]/.test(stripped);
  }

  _isCodeLike(line) {
    // Strong code signals only. The old rule matched any ()/[]/= which turned log
    // output and ordinary indented prose-with-parentheses into code blocks. Require
    // braces/semicolons, an operator, a code keyword, or an indented call instead.
    return /[{};]/.test(line) ||
      /=>|::|==|!=|<=|>=|&&|\|\|/.test(line) ||
      /\b(const|let|var|function|class|import|export|return|def|async|await)\b/.test(line) ||
      /^\s+\w[\w.]*\s*\(/.test(line);
  }

  _detectSelectionRanges(lines) {
    // Skip trailing empty lines (terminal padding) to find real content end
    let end = lines.length - 1;
    while (end >= 0 && /^\s*$/.test(this._stripAnsi(lines[end]))) end--;

    const BOTTOM = 30;
    const start = Math.max(0, end - BOTTOM + 1);
    const OPTION_RE = /^\s*(\d+|[a-z])[.)]\s+\S/;
    // ❯ N. text — cursor-prefixed option (the currently focused selection item)
    const CURSOR_OPTION_RE = /^\s*\u276F\s*(\d+|[a-z])[.)]\s+\S/;
    // Markdown formatting signals response text, not a real selection prompt
    const HAS_MARKDOWN = /\*\*|`[^\s`]/;
    const ranges = [];

    let rStart = -1;
    let count = 0;

    for (let i = start; i <= end; i++) {
      const stripped = this._stripAnsi(lines[i]);
      if ((OPTION_RE.test(stripped) || CURSOR_OPTION_RE.test(stripped)) && !HAS_MARKDOWN.test(stripped)) {
        if (rStart === -1) rStart = i;
        count++;
      } else {
        if (rStart !== -1 && count >= 2) {
          ranges.push({ start: rStart, end: i - 1 });
        }
        rStart = -1;
        count = 0;
      }
    }
    if (rStart !== -1 && count >= 2) {
      ranges.push({ start: rStart, end });
    }

    return ranges.filter(r => {
      // Allow only UI chrome after options: ❯ cursor, option-like hint lines ("4. Type here..."),
      // and empty lines. Reject if followed by real prose/tool output.
      const hasTrailingContent = lines.slice(r.end + 1).some(l => {
        const s = this._stripAnsi(l).trim();
        if (!s) return false;                    // empty line — ignore
        if (/^\u276F/.test(s)) return false;     // ❯ cursor — ignore
        if (OPTION_RE.test(s)) return false;     // another option-like hint — ignore
        return true;
      });
      if (hasTrailingContent) return false;

      // Option text should be reasonably short — real choices are concise.
      // Result descriptions are longer ("Updated 5 files in src/...", etc.)
      const optLines = lines.slice(r.start, r.end + 1);
      let totalLen = 0;
      for (const l of optLines) {
        const m = this._stripAnsi(l).match(/^\s*(?:\d+|[a-z])[.)]\s+(.+)/);
        if (m) totalLen += m[1].trim().length;
      }
      if (totalLen / optLines.length > 100) return false;

      return true;
    });
  }

  _parseOptions(lines) {
    const opts = [];
    for (const line of lines) {
      const s = this._stripAnsi(line);
      const focused = /^\s*\u276F/.test(s);
      const m = s.match(/^\s*\u276F?\s*(\d+|[a-z])[.)]\s+(.+)/);
      if (m) opts.push({ index: m[1], label: m[2].trim(), key: m[1], focused });
    }
    return opts;
  }
}

window.ClaudeParser = ClaudeParser;
