#!/usr/bin/env node
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
  checkBudgetBaseline,
  cliHexColorDistance,
  cliRgbToHex,
  normalizeCliHexColor,
  writeReportJson,
} from './theme-color-audit-utils.mjs';

const DEFAULT_ROOT = 'src/apps/cli';
const DEFAULT_BASELINE = 'scripts/theme-color-governance-baseline.cli.json';
const DEFAULT_NEAR_THRESHOLD = 10;

export { normalizeCliHexColor as normalizeHexColor, writeReportJson } from './theme-color-audit-utils.mjs';

export const CLI_RUNTIME_THEME_KEYS = new Set([
  'primary',
  'success',
  'warning',
  'error',
  'info',
  'textMuted',
  'background',
  'border',
  'backgroundPanel',
  'backgroundElement',
  'inputBackground',
  'diffAdded',
  'diffRemoved',
  'diffAddedBg',
  'diffRemovedBg',
  'borderActive',
  'accent',
  'diffHunkHeader',
  'diffLineNumber',
]);

function baseThemeKey(key) {
  return key.replace(/\.(dark|light)$/, '');
}

export function isRuntimePresetEntry(entry) {
  return CLI_RUNTIME_THEME_KEYS.has(baseThemeKey(entry.key));
}

export function collectPresetColorEntriesFromJson(file, jsonText) {
  const parsed = JSON.parse(jsonText);
  const theme = parsed.theme;
  if (!theme || typeof theme !== 'object' || Array.isArray(theme)) {
    throw new Error(`${file} must contain a theme object`);
  }
  const defs = parsed.defs && typeof parsed.defs === 'object' && !Array.isArray(parsed.defs)
    ? parsed.defs
    : {};
  const defaultMode = /(?:^|[-_/])light(?:[-_.]|$)/i.test(file) ? 'light' : 'dark';

  return Object.entries(theme).flatMap(([key, value]) => {
    return collectColorValueEntries({
      file,
      key,
      value,
      theme,
      defs,
      mode: defaultMode,
      seen: new Set(),
    });
  });
}

function collectColorValueEntries({ file, key, value, theme, defs, mode, seen }) {
  if (typeof value === 'number') {
    return [];
  }
  if (typeof value === 'string') {
    const trimmed = value.trim();
    if (trimmed === '' || trimmed.toLowerCase() === 'none' || trimmed.toLowerCase() === 'transparent') {
      return [];
    }
    const color = normalizeCliHexColor(trimmed, { mode });
    if (color) {
      return [{ file, key, color }];
    }

    const referenced = Object.prototype.hasOwnProperty.call(defs, trimmed)
      ? defs[trimmed]
      : Object.prototype.hasOwnProperty.call(theme, trimmed)
        ? theme[trimmed]
        : undefined;
    if (referenced === undefined) {
      return [];
    }
    if (seen.has(trimmed)) {
      throw new Error(`${file} theme color reference cycle detected at "${trimmed}"`);
    }
    const nextSeen = new Set(seen);
    nextSeen.add(trimmed);
    return collectColorValueEntries({
      file,
      key,
      value: referenced,
      theme,
      defs,
      mode,
      seen: nextSeen,
    });
  }
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
    return [];
  }
  if (Object.prototype.hasOwnProperty.call(value, 'dark') && Object.prototype.hasOwnProperty.call(value, 'light')) {
    return [
      ...collectColorValueEntries({
        file,
        key: `${key}.dark`,
        value: value.dark,
        theme,
        defs,
        mode: 'dark',
        seen: new Set(seen),
      }),
      ...collectColorValueEntries({
        file,
        key: `${key}.light`,
        value: value.light,
        theme,
        defs,
        mode: 'light',
        seen: new Set(seen),
      }),
    ];
  }

  return [];
}

export function collectRustFallbackEntriesFromText(file, sourceText) {
  const entries = [];
  const pattern = /([a-zA-Z_][a-zA-Z0-9_]*):\s*Color::Rgb\(\s*(\d+),\s*(\d+),\s*(\d+)\s*\)/g;
  let match;
  while ((match = pattern.exec(sourceText)) !== null) {
    entries.push({
      file,
      key: match[1],
      color: cliRgbToHex(
        Number.parseInt(match[2], 10),
        Number.parseInt(match[3], 10),
        Number.parseInt(match[4], 10),
      ),
    });
  }
  return entries;
}

export function findNearPairs(entries, threshold = DEFAULT_NEAR_THRESHOLD) {
  const filesByColor = new Map();
  const keysByColor = new Map();
  for (const entry of entries) {
    if (!filesByColor.has(entry.color)) {
      filesByColor.set(entry.color, new Set());
      keysByColor.set(entry.color, new Set());
    }
    filesByColor.get(entry.color).add(entry.file);
    keysByColor.get(entry.color).add(`${entry.file}:${entry.key}`);
  }

  const colors = Array.from(filesByColor.keys()).sort();
  const pairs = [];
  for (let i = 0; i < colors.length; i += 1) {
    for (let j = i + 1; j < colors.length; j += 1) {
      const distance = cliHexColorDistance(colors[i], colors[j]);
      if (distance > 0 && distance <= threshold) {
        pairs.push({
          a: colors[i],
          b: colors[j],
          distance: Number(distance.toFixed(2)),
          files: Array.from(new Set([
            ...filesByColor.get(colors[i]),
            ...filesByColor.get(colors[j]),
          ])).sort(),
          keysByColor: {
            [colors[i]]: Array.from(keysByColor.get(colors[i])).sort(),
            [colors[j]]: Array.from(keysByColor.get(colors[j])).sort(),
          },
        });
      }
    }
  }

  pairs.sort((left, right) => left.distance - right.distance || left.a.localeCompare(right.a));
  return pairs;
}

function countByColor(entries) {
  const counts = new Map();
  for (const entry of entries) {
    counts.set(entry.color, (counts.get(entry.color) ?? 0) + 1);
  }
  return Array.from(counts.entries())
    .map(([color, count]) => ({ color, count }))
    .sort((a, b) => b.count - a.count || a.color.localeCompare(b.color));
}

function listPresetFiles(root) {
  const presetDir = path.join(root, 'themes', 'presets');
  return fs.readdirSync(presetDir)
    .filter(file => file.endsWith('.json'))
    .sort()
    .map(file => path.join(presetDir, file));
}

function readPresetEntries(files) {
  return files.flatMap(file => {
    return collectPresetColorEntriesFromJson(
      path.relative(process.cwd(), file).replaceAll('\\', '/'),
      fs.readFileSync(file, 'utf8'),
    );
  });
}

function readRustFallbackEntries(root) {
  const fullPath = path.join(root, 'src', 'ui', 'theme.rs');
  return collectRustFallbackEntriesFromText(
    path.relative(process.cwd(), fullPath).replaceAll('\\', '/'),
    fs.readFileSync(fullPath, 'utf8'),
  );
}

export function createCliThemeColorReport(options = {}) {
  const root = options.root ?? DEFAULT_ROOT;
  const threshold = options.nearThreshold ?? DEFAULT_NEAR_THRESHOLD;
  const presetFiles = listPresetFiles(root);
  const presetEntries = readPresetEntries(presetFiles);
  const runtimePresetEntries = presetEntries.filter(isRuntimePresetEntry);
  const compatibilityPresetEntries = presetEntries.filter(entry => !isRuntimePresetEntry(entry));
  const rustFallbackEntries = readRustFallbackEntries(root);
  const allEntries = [...presetEntries, ...rustFallbackEntries];
  const runtimeEntries = [...runtimePresetEntries, ...rustFallbackEntries];
  const presetNearPairs = findNearPairs(presetEntries, threshold);
  const runtimePresetNearPairs = findNearPairs(runtimePresetEntries, threshold);
  const compatibilityPresetNearPairs = findNearPairs(compatibilityPresetEntries, threshold);
  const rustFallbackNearPairs = findNearPairs(rustFallbackEntries, threshold);

  return {
    root,
    presetFiles: presetFiles.length,
    presetColorOccurrences: presetEntries.length,
    presetUniqueColors: new Set(presetEntries.map(entry => entry.color)).size,
    runtimePresetColorOccurrences: runtimePresetEntries.length,
    runtimePresetUniqueColors: new Set(runtimePresetEntries.map(entry => entry.color)).size,
    compatibilityPresetColorOccurrences: compatibilityPresetEntries.length,
    compatibilityPresetUniqueColors: new Set(compatibilityPresetEntries.map(entry => entry.color)).size,
    rustFallbackColorOccurrences: rustFallbackEntries.length,
    rustFallbackUniqueColors: new Set(rustFallbackEntries.map(entry => entry.color)).size,
    totalUniqueColors: new Set(allEntries.map(entry => entry.color)).size,
    runtimeTotalUniqueColors: new Set(runtimeEntries.map(entry => entry.color)).size,
    nearThreshold: threshold,
    runtimeThemeKeys: Array.from(CLI_RUNTIME_THEME_KEYS).sort(),
    presetNearPairs: {
      nearTotal: presetNearPairs.length,
      near: presetNearPairs,
    },
    runtimePresetNearPairs: {
      nearTotal: runtimePresetNearPairs.length,
      near: runtimePresetNearPairs,
    },
    compatibilityPresetNearPairs: {
      nearTotal: compatibilityPresetNearPairs.length,
      near: compatibilityPresetNearPairs,
    },
    rustFallbackNearPairs: {
      nearTotal: rustFallbackNearPairs.length,
      near: rustFallbackNearPairs,
    },
    topPresetColors: countByColor(presetEntries),
    topRuntimePresetColors: countByColor(runtimePresetEntries),
    topCompatibilityPresetColors: countByColor(compatibilityPresetEntries),
    topRustFallbackColors: countByColor(rustFallbackEntries),
  };
}

function parseArgs(argv) {
  const options = {
    root: DEFAULT_ROOT,
    baseline: DEFAULT_BASELINE,
    json: false,
    reportJson: null,
    top: 20,
  };

  for (let i = 0; i < argv.length; i += 1) {
    const arg = argv[i];
    if (arg === '--root') {
      options.root = argv[++i];
      if (!options.root) {
        throw new Error('--root requires a path');
      }
    } else if (arg.startsWith('--root=')) {
      options.root = arg.slice('--root='.length);
      if (!options.root) {
        throw new Error('--root requires a path');
      }
    } else if (arg === '--baseline') {
      options.baseline = argv[++i];
      if (!options.baseline) {
        throw new Error('--baseline requires a baseline path');
      }
    } else if (arg.startsWith('--baseline=')) {
      options.baseline = arg.slice('--baseline='.length);
      if (!options.baseline) {
        throw new Error('--baseline requires a baseline path');
      }
    } else if (arg === '--report-json') {
      options.reportJson = argv[++i];
      if (!options.reportJson) {
        throw new Error('--report-json requires an output path');
      }
    } else if (arg.startsWith('--report-json=')) {
      options.reportJson = arg.slice('--report-json='.length);
      if (!options.reportJson) {
        throw new Error('--report-json requires an output path');
      }
    } else if (arg === '--json') {
      options.json = true;
    } else if (arg === '--top') {
      options.top = Number.parseInt(argv[++i], 10);
    } else if (arg.startsWith('--top=')) {
      options.top = Number.parseInt(arg.slice('--top='.length), 10);
    } else if (arg === '--no-baseline') {
      options.baseline = null;
    } else {
      throw new Error(`Unknown argument: ${arg}`);
    }
  }

  return options;
}

export function checkBaseline(report, baselinePath) {
  return checkBudgetBaseline(report, baselinePath);
}

function printRows(title, rows, top) {
  console.log(title);
  if (rows.length === 0) {
    console.log('  none');
    return;
  }
  for (const row of rows.slice(0, top)) {
    if ('count' in row) {
      console.log(`${String(row.count).padStart(7)}  ${row.color}`);
    } else {
      console.log(`  ${row.a} <-> ${row.b} distance=${row.distance} files=${row.files.join(', ')}`);
    }
  }
}

function printReport(report, top) {
  console.log(`CLI theme color audit: ${report.root}`);
  console.log(`Preset files: ${report.presetFiles}`);
  console.log(`Preset color occurrences: ${report.presetColorOccurrences}`);
  console.log(`Preset unique colors: ${report.presetUniqueColors}`);
  console.log(`Runtime-consumed preset color occurrences: ${report.runtimePresetColorOccurrences}`);
  console.log(`Runtime-consumed preset unique colors: ${report.runtimePresetUniqueColors}`);
  console.log(`Compatibility-declared preset color occurrences: ${report.compatibilityPresetColorOccurrences}`);
  console.log(`Compatibility-declared preset unique colors: ${report.compatibilityPresetUniqueColors}`);
  console.log(`Rust fallback Color::Rgb occurrences: ${report.rustFallbackColorOccurrences}`);
  console.log(`Rust fallback unique colors: ${report.rustFallbackUniqueColors}`);
  console.log(`Total CLI unique colors: ${report.totalUniqueColors}`);
  console.log(`Runtime CLI unique colors: ${report.runtimeTotalUniqueColors}`);
  console.log(`Preset near pairs: ${report.presetNearPairs.nearTotal}`);
  console.log(`Runtime-consumed preset near pairs: ${report.runtimePresetNearPairs.nearTotal}`);
  console.log(`Compatibility-declared preset near pairs: ${report.compatibilityPresetNearPairs.nearTotal}`);
  console.log(`Rust fallback near pairs: ${report.rustFallbackNearPairs.nearTotal}`);
  console.log('');
  printRows('Top preset colors:', report.topPresetColors, top);
  console.log('');
  printRows('Top runtime-consumed preset colors:', report.topRuntimePresetColors, top);
  console.log('');
  printRows('Top compatibility-declared preset colors:', report.topCompatibilityPresetColors, top);
  console.log('');
  printRows('Top Rust fallback colors:', report.topRustFallbackColors, top);
  console.log('');
  printRows('Preset near pairs:', report.presetNearPairs.near, Math.min(top, 20));
  console.log('');
  printRows('Runtime-consumed preset near pairs:', report.runtimePresetNearPairs.near, Math.min(top, 20));
  console.log('');
  printRows('Compatibility-declared preset near pairs:', report.compatibilityPresetNearPairs.near, Math.min(top, 20));
  console.log('');
  printRows('Rust fallback near pairs:', report.rustFallbackNearPairs.near, Math.min(top, 20));
}

function main() {
  const options = parseArgs(process.argv.slice(2));
  const report = createCliThemeColorReport({ root: options.root });
  if (options.json) {
    console.log(JSON.stringify(report, null, 2));
  } else {
    printReport(report, options.top);
  }

  if (options.reportJson) {
    writeReportJson(report, options.reportJson);
  }

  const failures = checkBaseline(report, options.baseline);
  if (failures.length > 0) {
    console.error('');
    console.error('CLI theme color audit failed:');
    for (const failure of failures) {
      console.error(`- ${failure}`);
    }
    process.exitCode = 1;
  }
}

if (process.argv[1] === fileURLToPath(import.meta.url)) {
  main();
}