#!/usr/bin/env node
/**
 * OpenClaw Context — ctx_compress
 *
 * One-stop content-aware compression tool. Detects content type,
 * selects optimal compressor, applies CCR markers for reversibility.
 *
 * Integrates: detector.js + smart-crusher.js + ccr-store.js
 *
 * Usage:
 *   cat huge_output.json | node ctx-compress.js
 *   → compressed content + metadata
 *
 *   node ctx-compress.js '{"content":"[1,2,3]","contentType":"json","bias":0.7}'
 *   → { compressed, strategy, savings, ccrHash, ... }
 */

const { detectType, CONTENT_TYPES } = require('./detector');
const { compressContent, crushArray } = require('./smart-crusher');
const { CcrStore, getCcrStore, ccrHash } = require('./ccr-store');

// ─── Log Compressor ──────────────────────────────────────────────

/**
 * Build output / log compressor.
 * Template extraction + dedup for repetitive build/CI output.
 */
function compressLog(text, options = {}) {
  const config = { maxLines: 50, templateThreshold: 3, ...options };
  const lines = text.split('\n');

  if (lines.length <= config.maxLines) {
    return { compressed: text, strategy: 'passthrough:small_log', savings: 0 };
  }

  // Extract template patterns
  const patterns = new Map();

  for (const line of lines) {
    const trimmed = line.trim();
    if (!trimmed) continue;

    // Detect templatable patterns
    const cleaned = trimmed
      .replace(/\d+/g, '{N}')           // numbers
      .replace(/0x[0-9a-f]+/gi, '{HEX}') // hex
      .replace(/\/[^\s]+\/[^\s]*/g, '{PATH}') // paths
      .replace(/\d+\.\d+\.\d+/g, '{VER}'); // versions

    patterns.set(cleaned, (patterns.get(cleaned) || 0) + 1);
  }

  // Find repetitive patterns (>templateThreshold occurrences)
  const templates = [...patterns.entries()]
    .filter(([, count]) => count >= config.templateThreshold)
    .sort(([, a], [, b]) => b - a);

  if (templates.length === 0) {
    return { compressed: text, strategy: 'passthrough:no_templates', savings: 0 };
  }

  // Build compressed output: templates + unique lines
  const templateSet = new Set(templates.map(([p]) => p));
  const outputLines = [];
  const seenTemplates = new Set();

  // First pass: collect unique lines + template first occurrence
  for (const line of lines) {
    const trimmed = line.trim();
    const cleaned = trimmed
      .replace(/\d+/g, '{N}')
      .replace(/0x[0-9a-f]+/gi, '{HEX}')
      .replace(/\/[^\s]+\/[^\s]*/g, '{PATH}')
      .replace(/\d+\.\d+\.\d+/g, '{VER}');

    if (templateSet.has(cleaned)) {
      if (!seenTemplates.has(cleaned)) {
        outputLines.push(trimmed + ` [×${patterns.get(cleaned)}]`);
        seenTemplates.add(cleaned);
      }
    } else {
      outputLines.push(trimmed);
    }
  }

  const compressed = outputLines.join('\n');
  const savings = Math.round((1 - compressed.length / text.length) * 10000) / 100;

  return {
    compressed,
    strategy: 'log:template_dedup',
    savings,
    originalLines: lines.length,
    compressedLines: outputLines.length,
    templatesFound: templates.length,
  };
}

// ─── Search Results Compressor ───────────────────────────────────

function compressSearchResults(text, options = {}) {
  const config = { maxResults: 30, ...options };
  const lines = text.split('\n').filter(l => l.trim());

  if (lines.length <= config.maxResults) {
    return { compressed: text, strategy: 'passthrough:small_search', savings: 0 };
  }

  // Dedup by file path (first field before ':')
  const byFile = new Map();
  for (const line of lines) {
    const match = line.match(/^([^:]+):/);
    const file = match ? match[1] : line;
    if (!byFile.has(file)) byFile.set(file, []);
    byFile.get(file).push(line);
  }

  // Take first result per file, up to maxResults
  const selected = [];
  for (const [file, fileLines] of byFile) {
    selected.push(fileLines[0]);
    if (selected.length >= config.maxResults) break;
  }

  const compressed = selected.join('\n') +
    (lines.length > selected.length
      ? `\n// ... ${lines.length - selected.length} more results across ${byFile.size} files`
      : '');

  const savings = Math.round((1 - compressed.length / text.length) * 10000) / 100;

  return {
    compressed,
    strategy: 'search:file_dedup',
    savings,
    originalLines: lines.length,
    keptLines: selected.length,
    filesMatched: byFile.size,
  };
}

// ─── Git Diff Compressor ─────────────────────────────────────────

function compressDiff(text, options = {}) {
  const config = { keepAllChanges: true, ...options };

  // Split by file sections
  const fileSections = text.split(/^(?=diff --git )/m).filter(Boolean);

  if (fileSections.length <= 5) {
    return { compressed: text, strategy: 'passthrough:small_diff', savings: 0 };
  }

  // Classify sections
  const important = [];
  const noise = [];
  const noisePatterns = [
    /package-lock\.json/,
    /yarn\.lock/,
    /pnpm-lock\.yaml/,
    /Cargo\.lock/,
    /Gemfile\.lock/,
    /poetry\.lock/,
    /\.svg$/,
    /\.min\.(js|css)$/,
    /generated/i,
  ];

  for (const section of fileSections) {
    const header = section.split('\n')[0] || '';
    const isNoise = noisePatterns.some(p => p.test(header));
    if (isNoise) {
      noise.push(header.replace('diff --git ', ''));
    } else {
      important.push(section);
    }
  }

  if (noise.length === 0) {
    return { compressed: text, strategy: 'passthrough:all_important', savings: 0 };
  }

  const compressed = important.join('') +
    `\n// ${noise.length} noise files omitted (lock files, generated, minified):\n` +
    noise.map(n => `//   ${n}`).join('\n');

  const savings = Math.round((1 - compressed.length / text.length) * 10000) / 100;

  return {
    compressed,
    strategy: 'diff:noise_filtered',
    savings,
    keptFiles: important.length,
    omittedFiles: noise.length,
  };
}

// ─── Main ctx_compress Interface ─────────────────────────────────

/**
 * One-stop compression: detect type → compress → mark CCR.
 */
function ctxCompress(input, options = {}) {
  const { contentType, bias, query, enableCcr } = options;
  const text = typeof input === 'string' ? input : JSON.stringify(input);

  // Detect content type if not specified
  const detected = contentType || detectType(text).type;

  let result;

  switch (detected) {
    case CONTENT_TYPES.JSON_ARRAY:
    case 'json':
    case 'json_array':
      result = compressContent(text, { bias, query, enableCompression: true, enableCompaction: true });
      break;

    case CONTENT_TYPES.BUILD_OUTPUT:
    case CONTENT_TYPES.LOG_OUTPUT:
    case 'log':
    case 'build_output':
      result = compressLog(text, options);
      break;

    case CONTENT_TYPES.SEARCH_RESULTS:
    case 'search':
    case 'search_results':
      result = compressSearchResults(text, options);
      break;

    case CONTENT_TYPES.GIT_DIFF:
    case 'diff':
    case 'git_diff':
      result = compressDiff(text, options);
      break;

    case CONTENT_TYPES.PLAIN_TEXT:
    case 'text':
    default:
      // Try JSON array anyway
      try {
        const parsed = JSON.parse(text);
        if (Array.isArray(parsed) && parsed.length > 10) {
          result = compressContent(text, { bias, query });
          break;
        }
      } catch {}
      result = { compressed: text, strategy: 'passthrough:plain_text', savings: 0 };
  }

  // Integrate CCR store
  if (enableCcr !== false && result.ccrHash) {
    const ccrStore = getCcrStore();
    ccrStore.put(result.ccrHash, text);
  }

  return {
    ...result,
    contentType: detected,
    originalSize: text.length,
    compressedSize: typeof result.compressed === 'string'
      ? result.compressed.length
      : JSON.stringify(result.compressed).length,
    timestamp: new Date().toISOString(),
  };
}

// ─── CLI ─────────────────────────────────────────────────────────

function main() {
  const args = process.argv.slice(2);

  if (args.includes('--help') || args.includes('-h')) {
    console.log(`ctx_compress — Content-Aware Compression

Usage:
  cat large_output.json | node ctx-compress.js [--json] [--bias 0.7] [--type json]
  node ctx-compress.js '{"content":"...","contentType":"json","bias":0.7}'

Options:
  --json          Output full result as JSON (including metadata)
  --bias N        Compression bias (0=conservative, 1=aggressive)
  --type TYPE     Force content type (json|log|search|diff|text)
  --no-ccr        Disable CCR reversible storage
  --retrieve HASH Retrieve original content from CCR store

Types:
  json    — JSON arrays (SmartCrusher: lossless CSV or smart sample)
  log     — Build/log output (template extraction + dedup)
  search  — Search results (file-level dedup)
  diff    — Git diff (noise filtering)  
  text    — Plain text (passthrough, but try JSON first)`);
    return;
  }

  // Retrieve mode
  const retrieveIdx = args.indexOf('--retrieve');
  if (retrieveIdx >= 0) {
    const hash = args[retrieveIdx + 1];
    const store = getCcrStore();
    const data = store.get(hash);
    if (data) {
      console.log(data);
    } else {
      process.stderr.write('CCR entry not found or expired\n');
      process.exit(1);
    }
    return;
  }

  // JSON argument mode
  const jsonArg = args.find(a => a.startsWith('{'));
  if (jsonArg) {
    try {
      const opts = JSON.parse(jsonArg);
      const result = ctxCompress(opts.content || '', opts);
      console.log(JSON.stringify({
        compressed: typeof result.compressed === 'string'
          ? result.compressed
          : JSON.stringify(result.compressed),
        strategy: result.strategy,
        savings: result.savings + '%',
        originalSize: result.originalSize,
        compressedSize: result.compressedSize,
        ccrHash: result.ccrHash || null,
        contentType: result.contentType,
      }, null, 2));
    } catch (e) {
      console.log(JSON.stringify({ error: e.message }));
    }
    return;
  }

  // Stdin mode
  const biasIdx = args.indexOf('--bias');
  const bias = biasIdx >= 0 ? parseFloat(args[biasIdx + 1]) || 0.5 : 0.5;
  const typeIdx = args.indexOf('--type');
  const contentType = typeIdx >= 0 ? args[typeIdx + 1] : null;
  const queryIdx = args.indexOf('--query');
  const query = queryIdx >= 0 ? args[queryIdx + 1] : null;
  const enableCcr = !args.includes('--no-ccr');
  const jsonOutput = args.includes('--json');

  let input = '';
  process.stdin.setEncoding('utf8');
  process.stdin.on('data', chunk => input += chunk);
  process.stdin.on('end', () => {
    const result = ctxCompress(input, { contentType, bias, query, enableCcr });

    if (jsonOutput) {
      const { compressed, ...meta } = result;
      console.log(JSON.stringify({
        ...meta,
        savings: result.savings + '%',
        compressedPreview: typeof compressed === 'string'
          ? compressed.substring(0, 200)
          : JSON.stringify(compressed).substring(0, 200),
      }, null, 2));
    } else {
      console.log(typeof result.compressed === 'string'
        ? result.compressed
        : JSON.stringify(result.compressed));
    }
  });
}

module.exports = { ctxCompress, compressLog, compressSearchResults, compressDiff };

if (require.main === module) {
  main();
}
