import * as crypto from 'node:crypto';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { visibleWidth } from './utils.js';

const DEFAULT_PREVIEW_CHARS = 240;
const DEFAULT_CHANGED_LINE_LIMIT = 24;

function hashString(value) {
  return crypto.createHash('sha1').update(value).digest('hex').slice(0, 12);
}

function preview(value, maxChars = DEFAULT_PREVIEW_CHARS) {
  if (value.length <= maxChars) return value;
  return `${value.slice(0, maxChars)}...<${value.length - maxChars} more chars>`;
}

function summarizeTextPayload(value, includeRaw = false) {
  const summary = {
    hash: hashString(value),
    chars: value.length,
    bytes: Buffer.byteLength(value, 'utf8'),
    preview: preview(value)
  };
  if (includeRaw) {
    summary.raw = value;
  }
  return summary;
}

function lineSummary(line, index, includeText) {
  const summary = {
    index,
    hash: hashString(line),
    chars: line.length,
    width: visibleWidth(line),
    preview: preview(line)
  };
  if (includeText) {
    summary.text = line;
  }
  return summary;
}

function countMatches(value, pattern) {
  return value.match(pattern)?.length ?? 0;
}

function parseCount(value, fallback = 1) {
  if (!value) return fallback;
  const parsed = Number.parseInt(value, 10);
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}

function summarizeCursorMovement(buffer) {
  const ops = [];
  let upCount = 0;
  let upRows = 0;
  let upMax = 0;
  let downCount = 0;
  let downRows = 0;
  let downMax = 0;
  let columnCount = 0;

  const movementPattern = /\x1b\[(\d*)([ABG])/g;
  for (const match of buffer.matchAll(movementPattern)) {
    const amount = parseCount(match[1]);
    const op = match[2];
    if (op === 'A') {
      upCount += 1;
      upRows += amount;
      upMax = Math.max(upMax, amount);
    } else if (op === 'B') {
      downCount += 1;
      downRows += amount;
      downMax = Math.max(downMax, amount);
    } else {
      columnCount += 1;
    }
    if (ops.length < 80) {
      ops.push({ op, amount });
    }
  }

  return {
    up: { count: upCount, rows: upRows, max: upMax },
    down: { count: downCount, rows: downRows, max: downMax },
    column: { count: columnCount },
    netRows: downRows - upRows,
    opsSample: ops,
    opsTruncated: upCount + downCount + columnCount > ops.length
  };
}

export function summarizeBuffer(buffer, includeRaw = false) {
  const cursorMovement = summarizeCursorMovement(buffer);
  const summary = {
    hash: hashString(buffer),
    chars: buffer.length,
    bytes: Buffer.byteLength(buffer, 'utf8'),
    carriageReturns: countMatches(buffer, /\r/g),
    newlines: countMatches(buffer, /\n/g),
    cursorUp: countMatches(buffer, /\x1b\[\d*A/g),
    cursorDown: countMatches(buffer, /\x1b\[\d*B/g),
    cursorColumn: countMatches(buffer, /\x1b\[\d*G/g),
    clearLine: countMatches(buffer, /\x1b\[(?:2)?K/g),
    clearScreen: countMatches(buffer, /\x1b\[(?:2|3)J/g),
    synchronizedOutputStart: countMatches(buffer, /\x1b\[\?2026h/g),
    synchronizedOutputEnd: countMatches(buffer, /\x1b\[\?2026l/g),
    cursorMovement,
    preview: preview(buffer)
  };
  if (includeRaw) {
    summary.raw = buffer;
  }
  return summary;
}

export function summarizeLines(previousLines, newLines, options) {
  const {
    height,
    firstChanged = -1,
    lastChanged = -1,
    includeFullText = false,
    changedLineLimit = DEFAULT_CHANGED_LINE_LIMIT,
    rendererPreviousViewportTop,
    rendererNextViewportTop
  } = options;

  const previousViewportTop = Math.max(0, previousLines.length - height);
  const newViewportTop = Math.max(0, newLines.length - height);
  const actualPreviousViewportTop = Math.max(0, rendererPreviousViewportTop ?? previousViewportTop);
  const actualNextViewportTop = Math.max(0, rendererNextViewportTop ?? newViewportTop);
  const maxLines = Math.max(previousLines.length, newLines.length);
  const changed = [];

  if (firstChanged !== -1) {
    for (let i = firstChanged; i <= lastChanged && i < maxLines; i += 1) {
      const oldLine = i < previousLines.length ? previousLines[i] : '';
      const newLine = i < newLines.length ? newLines[i] : '';
      if (oldLine === newLine && i < previousLines.length && i < newLines.length) {
        continue;
      }
      changed.push({
        index: i,
        old: lineSummary(oldLine, i, includeFullText),
        next: lineSummary(newLine, i, includeFullText)
      });
      if (changed.length >= changedLineLimit) {
        break;
      }
    }
  }

  return {
    previous: {
      count: previousLines.length,
      viewportTop: previousViewportTop,
      viewport: previousLines
        .slice(previousViewportTop, previousViewportTop + height)
        .map((line, offset) => lineSummary(line, previousViewportTop + offset, includeFullText))
    },
    next: {
      count: newLines.length,
      viewportTop: newViewportTop,
      viewport: newLines
        .slice(newViewportTop, newViewportTop + height)
        .map((line, offset) => lineSummary(line, newViewportTop + offset, includeFullText)),
      hashes: newLines.slice(newViewportTop, newViewportTop + height).map((line, offset) => ({
        index: newViewportTop + offset,
        hash: hashString(line),
        width: visibleWidth(line)
      }))
    },
    rendererViewport: {
      previousTop: actualPreviousViewportTop,
      previous: previousLines
        .slice(actualPreviousViewportTop, actualPreviousViewportTop + height)
        .map((line, offset) =>
          lineSummary(line, actualPreviousViewportTop + offset, includeFullText)
        ),
      nextTop: actualNextViewportTop,
      next: newLines
        .slice(actualNextViewportTop, actualNextViewportTop + height)
        .map((line, offset) => lineSummary(line, actualNextViewportTop + offset, includeFullText))
    },
    changed: {
      first: firstChanged,
      last: lastChanged,
      sampled: changed,
      sampleLimit: changedLineLimit,
      truncated: firstChanged !== -1 && lastChanged - firstChanged + 1 > changed.length
    }
  };
}

export function sanitizeOverlayOptions(options) {
  if (!options) return undefined;
  const sanitized = {};
  for (const [key, value] of Object.entries(options)) {
    sanitized[key] =
      typeof value === 'function' ? `[function ${value.name || 'anonymous'}]` : value;
  }
  return sanitized;
}

export class RenderTrace {
  constructor(tracePath, options = {}) {
    this.path = path.resolve(tracePath);
    this.includeFullText = options.includeFullText ?? false;
    this.sequence = 0;
    fs.mkdirSync(path.dirname(this.path), { recursive: true });
    this.event('trace_start', {
      pid: process.pid,
      cwd: process.cwd(),
      platform: process.platform,
      node: process.version,
      includeFullText: this.includeFullText
    });
  }

  recordEvent(type, payload = {}) {
    const entry = { ...payload };
    if (typeof entry.inputRaw === 'string') {
      entry.input = summarizeTextPayload(entry.inputRaw, this.includeFullText);
      delete entry.inputRaw;
    }
    if (typeof entry.bufferRaw === 'string') {
      entry.buffer = summarizeBuffer(entry.bufferRaw, this.includeFullText);
      delete entry.bufferRaw;
    }
    this.event(type, entry);
  }

  recordFrame(payload) {
    const entry = { ...payload };
    const previousLines = Array.isArray(entry.previousLinesRaw) ? entry.previousLinesRaw : [];
    const nextLines = Array.isArray(entry.newLinesRaw) ? entry.newLinesRaw : [];
    const height =
      typeof entry.height === 'number' && Number.isFinite(entry.height) ? entry.height : 24;

    entry.lines =
      entry.lines ??
      summarizeLines(previousLines, nextLines, {
        height,
        firstChanged:
          typeof entry.firstChanged === 'number' && Number.isFinite(entry.firstChanged)
            ? entry.firstChanged
            : -1,
        lastChanged:
          typeof entry.lastChanged === 'number' && Number.isFinite(entry.lastChanged)
            ? entry.lastChanged
            : -1,
        includeFullText: this.includeFullText,
        rendererPreviousViewportTop:
          typeof entry.rendererPreviousViewportTop === 'number' &&
          Number.isFinite(entry.rendererPreviousViewportTop)
            ? entry.rendererPreviousViewportTop
            : undefined,
        rendererNextViewportTop:
          typeof entry.rendererNextViewportTop === 'number' &&
          Number.isFinite(entry.rendererNextViewportTop)
            ? entry.rendererNextViewportTop
            : undefined
      });

    if (typeof entry.bufferRaw === 'string') {
      entry.buffer = summarizeBuffer(entry.bufferRaw, this.includeFullText);
      delete entry.bufferRaw;
    }

    delete entry.previousLinesRaw;
    delete entry.newLinesRaw;
    delete entry.rendererPreviousViewportTop;
    delete entry.rendererNextViewportTop;

    this.event('render_frame', entry);
  }

  event(type, payload = {}) {
    const entry = {
      seq: ++this.sequence,
      ts: new Date().toISOString(),
      type,
      ...payload
    };
    try {
      fs.appendFileSync(this.path, `${JSON.stringify(entry)}\n`, { encoding: 'utf8' });
    } catch {
      // Rendering diagnostics must never break the renderer.
    }
  }
}
