#!/usr/bin/env node
/*
 * Rendered visual contract for AI observability pages.
 *
 * This complements the static template guard by checking computed CSS in a
 * browser: typography, warm-token surfaces, dot-matrix texture, radius, and
 * mobile density across the 10 observability pages.
 */

const fs = require('fs');
const os = require('os');
const path = require('path');
const Module = require('module');

const DEFAULT_BASE_URL = 'https://mo.wooo.work';
const ROUTES = [
  '/observability/overview',
  '/observability/agent_orchestration',
  '/observability/business_intel',
  '/observability/host_health',
  '/observability/ai_calls',
  '/observability/budget',
  '/observability/promotion_review',
  '/observability/rag_queries',
  '/observability/quality_trend',
  '/observability/ppt_audit_history',
];

const VIEWPORTS = [
  { name: 'desktop-1440', width: 1440, height: 950, maxHeroHeight: 430, maxTitleSize: 30 },
  { name: 'tablet-630', width: 630, height: 968, maxHeroHeight: 520, maxTitleSize: 30 },
  { name: 'mobile-390', width: 390, height: 844, maxHeroHeight: 560, maxTitleSize: 26 },
];

const HERO_SELECTORS = [
  '.obs-hero',
  '.agent-hero',
  '.biz-command',
  '.runtime-hero',
  '.calls-hero',
  '.gov-hero',
  '.gate-hero',
  '.rag-hero',
  '.qa-hero',
  '.quality-hero',
  '.ppt-hero',
].join(',');

const TITLE_SELECTORS = [
  '.obs-title',
  '.agent-title',
  '.biz-title',
  '.runtime-title',
  '.calls-title',
  '.gov-title',
  '.gate-title',
  '.rag-title',
  '.qa-title',
  '.quality-title',
  '.ppt-title',
  '.container-fluid > h2:first-child',
].join(',');

const SURFACE_SELECTORS = [
  HERO_SELECTORS,
  '.obs-panel',
  '.agent-panel',
  '.biz-panel',
  '.runtime-panel',
  '.calls-panel',
  '.gov-panel',
  '.gate-panel',
  '.rag-panel',
  '.qa-panel',
  '.quality-panel',
  '.ppt-panel',
  '.obs-signal',
  '.agent-signal',
  '.biz-signal',
  '.runtime-signal',
  '.calls-signal',
  '.gov-signal',
  '.gate-signal',
  '.rag-signal',
  '.qa-signal',
  '.quality-signal',
  '.ppt-signal',
  '.biz-filter-card',
  '.biz-strategy-card',
  '.episode-card',
  '.host-lane',
  '.caller-card',
  '.agent-card',
  '.rec-card',
  '.fix-card',
  '.root-card',
  '.strategy-card',
].join(',');

function parseArgs(argv) {
  const options = { baseUrl: DEFAULT_BASE_URL, routes: [], json: false, timeoutMs: 30000 };
  for (let i = 0; i < argv.length; i += 1) {
    const arg = argv[i];
    if (arg === '--base-url') {
      options.baseUrl = argv[++i];
    } else if (arg === '--route') {
      options.routes.push(argv[++i]);
    } else if (arg === '--routes') {
      options.routes.push(...argv[++i].split(',').map((item) => item.trim()).filter(Boolean));
    } else if (arg === '--timeout') {
      options.timeoutMs = parseInt(argv[++i], 10) * 1000;
    } else if (arg === '--json') {
      options.json = true;
    } else if (arg === '--help' || arg === '-h') {
      printHelp();
      process.exit(0);
    } else {
      throw new Error(`Unknown argument: ${arg}`);
    }
  }
  if (options.routes.length === 0) {
    options.routes = ROUTES;
  }
  return options;
}

function printHelp() {
  console.log(`AI observability rendered visual contract

Options:
  --base-url URL      Base URL, default ${DEFAULT_BASE_URL}
  --route PATH        Add one route, can be repeated
  --routes A,B,C      Add comma-separated routes
  --timeout SEC       Navigation timeout, default 30
  --json              Print JSON results
`);
}

function requirePlaywright() {
  try {
    return require('playwright');
  } catch (error) {
    const candidates = [
      process.env.PLAYWRIGHT_NODE_MODULE_DIR,
      process.env.NODE_PATH,
      path.join(os.homedir(), '.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules'),
    ].filter(Boolean);
    for (const candidate of candidates) {
      const packageJson = path.join(candidate, 'playwright', 'package.json');
      if (fs.existsSync(packageJson)) {
        return Module.createRequire(packageJson)('playwright');
      }
    }
    throw new Error('Cannot load playwright. Set NODE_PATH or PLAYWRIGHT_NODE_MODULE_DIR to a node_modules directory containing playwright.');
  }
}

function findChromeExecutable() {
  const candidates = [
    process.env.RESPONSIVE_CHROME_PATH,
    '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
    '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
    '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
  ].filter(Boolean);
  return candidates.find((candidate) => fs.existsSync(candidate)) || '';
}

function routeToUrl(baseUrl, route) {
  return new URL(route, baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`).toString();
}

function px(value) {
  return Number.parseFloat(String(value || '').replace('px', '')) || 0;
}

function parseRgb(value) {
  const match = String(value || '').match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
  if (!match) return null;
  return match.slice(1, 4).map((item) => parseInt(item, 10) / 255);
}

function luminance(rgb) {
  if (!rgb) return 0;
  const values = rgb.map((channel) => (
    channel <= 0.03928 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4
  ));
  return 0.2126 * values[0] + 0.7152 * values[1] + 0.0722 * values[2];
}

function contrastRatio(fg, bg) {
  const a = luminance(parseRgb(fg));
  const b = luminance(parseRgb(bg));
  const lighter = Math.max(a, b);
  const darker = Math.min(a, b);
  return (lighter + 0.05) / (darker + 0.05);
}

function isDotMatrixBackground(backgroundImage) {
  const value = String(backgroundImage || '');
  return value.includes('radial-gradient') && !value.includes('linear-gradient');
}

async function collectMetrics(page) {
  return page.evaluate(({ heroSelectors, titleSelectors, surfaceSelectors }) => {
    const parseRgbValue = (value) => {
      const match = String(value || '').match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([0-9.]+))?/);
      if (!match) return null;
      return {
        rgb: match.slice(1, 4).map((item) => parseInt(item, 10) / 255),
        alpha: match[4] === undefined ? 1 : Number.parseFloat(match[4]),
      };
    };
    const effectiveBackground = (el) => {
      let current = el;
      while (current && current.nodeType === Node.ELEMENT_NODE) {
        const color = getComputedStyle(current).backgroundColor;
        const parsed = parseRgbValue(color);
        if (parsed && parsed.alpha > 0.05) return color;
        current = current.parentElement;
      }
      return 'rgb(250, 246, 236)';
    };
    const luminanceValue = (parsed) => {
      if (!parsed) return 0;
      const values = parsed.rgb.map((channel) => (
        channel <= 0.03928 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4
      ));
      return 0.2126 * values[0] + 0.7152 * values[1] + 0.0722 * values[2];
    };
    const contrast = (fg, bg) => {
      const a = luminanceValue(parseRgbValue(fg));
      const b = luminanceValue(parseRgbValue(bg));
      const lighter = Math.max(a, b);
      const darker = Math.min(a, b);
      return (lighter + 0.05) / (darker + 0.05);
    };
    const width = window.innerWidth;
    const hero = document.querySelector(heroSelectors);
    const title = document.querySelector(titleSelectors);
    const badRadius = [];
    const badBackground = [];
    const missingMatrix = [];
    const badChipContrast = [];

    for (const el of document.querySelectorAll(`.momo-observability-mode :is(${surfaceSelectors})`)) {
      const style = getComputedStyle(el);
      const radius = parseFloat(style.borderTopLeftRadius) || 0;
      if (radius > 8.5) {
        badRadius.push({
          className: String(el.className || '').slice(0, 100),
          radius,
          text: String(el.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 80),
        });
      }
      const mustUseMatrix = el.matches(heroSelectors)
        || el.matches('.obs-panel,.agent-panel,.biz-panel,.runtime-panel,.calls-panel,.gov-panel,.gate-panel,.rag-panel,.qa-panel,.quality-panel,.ppt-panel')
        || el.matches('.obs-signal,.agent-signal,.biz-signal,.runtime-signal,.calls-signal,.gov-signal,.gate-signal,.rag-signal,.qa-signal,.quality-signal,.ppt-signal');
      const hasRadialMatrix = style.backgroundImage.includes('radial-gradient');
      const hasLegacyGradient = style.backgroundImage.includes('linear-gradient');
      if (mustUseMatrix && (!hasRadialMatrix || hasLegacyGradient)) {
        badBackground.push({
          className: String(el.className || '').slice(0, 100),
          backgroundImage: style.backgroundImage.slice(0, 100),
        });
      }
      if (mustUseMatrix && !hasRadialMatrix) {
        missingMatrix.push({
          className: String(el.className || '').slice(0, 100),
          text: String(el.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 80),
        });
      }
      if (badRadius.length >= 8 && badBackground.length >= 8 && missingMatrix.length >= 8) break;
    }

    for (const el of document.querySelectorAll('.momo-observability-mode :is(.badge,.obs-pill,[class$="-pill"],.biz-badge)')) {
      const style = getComputedStyle(el);
      const ratio = contrast(style.color, effectiveBackground(el));
      if (ratio < 3) {
        badChipContrast.push({
          className: String(el.className || '').slice(0, 100),
          text: String(el.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 60),
          ratio,
        });
      }
      if (badChipContrast.length >= 8) break;
    }

    const titleStyle = title ? getComputedStyle(title) : null;
    const heroStyle = hero ? getComputedStyle(hero) : null;
    return {
      mode: !!document.querySelector('.momo-observability-mode'),
      finalUrl: location.href,
      rootOverflow: Math.max(0, document.documentElement.scrollWidth - width),
      bodyOverflow: Math.max(0, document.body.scrollWidth - width),
      scrollHeight: document.documentElement.scrollHeight,
      hero: hero ? {
        className: String(hero.className || ''),
        height: Math.round(hero.getBoundingClientRect().height),
        radius: heroStyle.borderTopLeftRadius,
        backgroundImage: heroStyle.backgroundImage,
      } : null,
      title: title ? {
        text: String(title.textContent || '').trim().slice(0, 80),
        fontFamily: titleStyle.fontFamily,
        fontSize: titleStyle.fontSize,
        color: titleStyle.color,
        letterSpacing: titleStyle.letterSpacing,
        lineHeight: titleStyle.lineHeight,
      } : null,
      badRadius,
      badBackground,
      missingMatrix,
      badChipContrast,
    };
  }, { heroSelectors: HERO_SELECTORS, titleSelectors: TITLE_SELECTORS, surfaceSelectors: SURFACE_SELECTORS });
}

function issuesFor(metrics, viewport) {
  const issues = [];
  if (!metrics.mode) issues.push('missing momo-observability-mode');
  if (metrics.rootOverflow > 1 || metrics.bodyOverflow > 1) {
    issues.push(`horizontal overflow root=${metrics.rootOverflow}px body=${metrics.bodyOverflow}px`);
  }
  if (!metrics.title) {
    issues.push('missing title selector');
  } else {
    if (!/Inter|Noto Sans TC/.test(metrics.title.fontFamily)) {
      issues.push(`title font ${metrics.title.fontFamily}`);
    }
    if (px(metrics.title.fontSize) > viewport.maxTitleSize) {
      issues.push(`title too large ${metrics.title.fontSize}`);
    }
    if (metrics.title.letterSpacing !== 'normal' && px(metrics.title.letterSpacing) < -0.1) {
      issues.push(`title negative letter spacing ${metrics.title.letterSpacing}`);
    }
    if (metrics.title.color !== 'rgb(42, 37, 32)') {
      issues.push(`title color ${metrics.title.color}`);
    }
  }
  if (!metrics.hero) {
    issues.push('missing hero selector');
  } else {
    if (px(metrics.hero.radius) > 8.5) issues.push(`hero radius ${metrics.hero.radius}`);
    if (!isDotMatrixBackground(metrics.hero.backgroundImage)) {
      issues.push('hero missing tokenized dot-matrix background');
    }
    if (metrics.hero.height > viewport.maxHeroHeight) {
      issues.push(`hero too tall ${metrics.hero.height}px > ${viewport.maxHeroHeight}px`);
    }
  }
  if (metrics.badRadius.length) issues.push(`surface radius offenders ${metrics.badRadius.length}`);
  if (metrics.badBackground.length) issues.push(`surface non-matrix background offenders ${metrics.badBackground.length}`);
  if (metrics.missingMatrix.length) issues.push(`surface missing dot-matrix offenders ${metrics.missingMatrix.length}`);
  if (metrics.badChipContrast.length) issues.push(`chip contrast offenders ${metrics.badChipContrast.length}`);
  return issues;
}

async function main() {
  const options = parseArgs(process.argv.slice(2));
  const { chromium } = requirePlaywright();
  const launchOptions = { headless: true };
  const chromePath = findChromeExecutable();
  if (chromePath) launchOptions.executablePath = chromePath;

  const browser = await chromium.launch(launchOptions);
  const results = [];
  try {
    for (const route of options.routes) {
      for (const viewport of VIEWPORTS) {
        const page = await browser.newPage({
          viewport: { width: viewport.width, height: viewport.height },
          deviceScaleFactor: 1,
        });
        const url = routeToUrl(options.baseUrl, route);
        let status = 0;
        let error = '';
        let metrics = null;
        try {
          const response = await page.goto(url, { waitUntil: 'networkidle', timeout: options.timeoutMs });
          status = response ? response.status() : 0;
          metrics = await collectMetrics(page);
          if (new URL(metrics.finalUrl).pathname === '/login') {
            error = 'redirected to /login';
          } else if (status >= 400) {
            error = `HTTP ${status}`;
          }
        } catch (err) {
          error = err.message || String(err);
        }
        const issues = metrics ? issuesFor(metrics, viewport) : [];
        if (error) issues.unshift(error);
        results.push({ route, viewport: viewport.name, status, passed: issues.length === 0, issues, metrics });
        await page.close();
      }
    }
  } finally {
    await browser.close();
  }

  if (options.json) {
    console.log(JSON.stringify(results, null, 2));
  } else {
    for (const result of results) {
      const status = result.passed ? 'PASS' : 'FAIL';
      const title = result.metrics?.title ? `${result.metrics.title.fontSize} ${result.metrics.title.color}` : 'n/a';
      const hero = result.metrics?.hero ? `${result.metrics.hero.height}px r=${result.metrics.hero.radius}` : 'n/a';
      console.log(`${status} ${result.viewport} ${result.route} title=${title} hero=${hero}${result.issues.length ? ` issues=${result.issues.join('; ')}` : ''}`);
      if (!result.passed && result.metrics) {
        for (const bucket of ['badRadius', 'badBackground', 'missingMatrix', 'badChipContrast']) {
          for (const offender of result.metrics[bucket] || []) {
            console.log(`  ${bucket}: ${JSON.stringify(offender)}`);
          }
        }
      }
    }
  }

  if (results.some((result) => !result.passed)) {
    process.exitCode = 1;
  }
}

main().catch((error) => {
  console.error(error.message || error);
  process.exit(1);
});
