import fs from 'node:fs/promises';
import path from 'node:path';
import { workspaceThumbnailRelativePath } from './mindspace-workspace-thumbnails.mjs';

const FEED_WIDTH = 540;
const FEED_HEIGHT = 720;
const MAX_COVER_BYTES = 1.5 * 1024 * 1024;
const REMOTE_COVER_TIMEOUT_MS = 8000;

function escapeXml(value) {
  return String(value)
    .replaceAll('&', '&amp;')
    .replaceAll('<', '&lt;')
    .replaceAll('>', '&gt;')
    .replaceAll('"', '&quot;')
    .replaceAll("'", '&apos;');
}

function titleFromHtml(html) {
  const match = String(html).match(/<title[^>]*>([^<]+)<\/title>/i);
  return match?.[1]?.trim() ?? '';
}

function h1FromHtml(html) {
  const match = String(html).match(/<h1[^>]*>([\s\S]*?)<\/h1>/i);
  if (!match) return '';
  return match[1].replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();
}

function descriptionFromHtml(html) {
  const meta =
    String(html).match(/<meta[^>]+name=["']description["'][^>]+content=["']([^"']+)["']/i) ??
    String(html).match(/<meta[^>]+content=["']([^"']+)["'][^>]+name=["']description["']/i);
  if (meta?.[1]) return meta[1].trim();
  const paragraph = String(html).match(/<p[^>]*>([^<]{4,120})/i);
  return paragraph?.[1]?.trim() ?? '';
}

function parseCoverMeta(html) {
  const tag = String(html).match(/<meta[^>]*name=["']mindspace-cover["'][^>]*>/i)?.[0];
  if (!tag) return {};
  const contentMatch =
    tag.match(/content=(["'])([\s\S]*?)\1/i) ?? tag.match(/content=["']([^"']+)["']/i);
  const raw = contentMatch?.[2] ?? contentMatch?.[1];
  if (!raw) return {};
  try {
    return JSON.parse(raw.replaceAll('&quot;', '"'));
  } catch {
    return {};
  }
}

function extractEmoji(text) {
  const match = String(text).match(/\p{Extended_Pictographic}/u);
  return match?.[0] ?? '';
}

function coverImageFromHtml(html) {
  const source = String(html);
  const og =
    source.match(/<meta[^>]+property=["']og:image["'][^>]+content=["']([^"']+)["']/i) ??
    source.match(/<meta[^>]+content=["']([^"']+)["'][^>]+property=["']og:image["']/i);
  if (og?.[1]) return og[1].trim();
  const hero =
    source.match(/<img[^>]+class=["'][^"']*hero[^"']*["'][^>]+src=["']([^"']+)["']/i) ??
    source.match(/<img[^>]+src=["']([^"']+)["'][^>]+class=["'][^"']*hero/i);
  if (hero?.[1]) return hero[1].trim();
  const first = source.match(/<img[^>]+src=["']([^"']+)["']/i);
  return first?.[1]?.trim() ?? '';
}

function svgImageHref(value) {
  const raw = String(value);
  if (raw.startsWith('data:')) return raw;
  return escapeXml(raw);
}

function mimeFromImageBytes(buffer) {
  if (buffer[0] === 0xff && buffer[1] === 0xd8) return 'image/jpeg';
  if (buffer[0] === 0x89 && buffer[1] === 0x50) return 'image/png';
  if (buffer.length >= 12 && buffer.slice(0, 4).toString() === 'RIFF' && buffer.slice(8, 12).toString() === 'WEBP') {
    return 'image/webp';
  }
  if (buffer[0] === 0x47 && buffer[1] === 0x49) return 'image/gif';
  return 'image/jpeg';
}

function bytesToDataUri(buffer, mimeType) {
  return `data:${mimeType};base64,${buffer.toString('base64')}`;
}

export function bufferToImageDataUri(buffer) {
  if (!buffer?.length) return null;
  if (buffer.length > MAX_COVER_BYTES) return null;
  return bytesToDataUri(buffer, mimeFromImageBytes(buffer));
}

async function readLocalImageDataUri(filePath) {
  const buffer = await fs.readFile(filePath);
  if (buffer.length === 0 || buffer.length > MAX_COVER_BYTES) return null;
  return bytesToDataUri(buffer, mimeFromImageBytes(buffer));
}

async function fetchRemoteImageDataUri(url) {
  const response = await fetch(url, {
    signal: AbortSignal.timeout(REMOTE_COVER_TIMEOUT_MS),
    headers: { Accept: 'image/*' },
    redirect: 'follow',
  });
  if (!response.ok) return null;
  const contentType = response.headers.get('content-type') ?? '';
  if (contentType && !contentType.startsWith('image/')) return null;
  const buffer = Buffer.from(await response.arrayBuffer());
  if (buffer.length === 0 || buffer.length > MAX_COVER_BYTES) return null;
  const mimeType = contentType.startsWith('image/') ? contentType.split(';')[0] : mimeFromImageBytes(buffer);
  return bytesToDataUri(buffer, mimeType);
}

function resolveLocalImagePath({ storageRoot, contentStorageKey, contentBaseDir, imageUrl }) {
  if (!imageUrl || /^data:|^https?:/i.test(imageUrl)) return null;
  let baseDir = null;
  if (contentStorageKey) {
    baseDir = path.dirname(path.resolve(storageRoot, contentStorageKey));
    const root = path.resolve(storageRoot);
    if (baseDir !== root && !baseDir.startsWith(`${root}${path.sep}`)) return null;
  } else if (contentBaseDir) {
    baseDir = path.resolve(contentBaseDir);
  }
  if (!baseDir) return null;
  const target = path.resolve(baseDir, imageUrl.replace(/^\.\//, ''));
  if (target !== baseDir && !target.startsWith(`${baseDir}${path.sep}`)) return null;
  return target;
}

export async function resolveCoverDataUri({ storageRoot, contentStorageKey, contentBaseDir, imageUrl }) {
  const raw = String(imageUrl ?? '').trim();
  if (!raw) return null;
  if (raw.startsWith('data:')) return raw.length <= MAX_COVER_BYTES * 2 ? raw : null;
  if (/^https?:\/\//i.test(raw)) {
    try {
      return await fetchRemoteImageDataUri(raw);
    } catch {
      return null;
    }
  }
  const localPath = resolveLocalImagePath({ storageRoot, contentStorageKey, contentBaseDir, imageUrl: raw });
  if (!localPath) return null;
  try {
    return await readLocalImageDataUri(localPath);
  } catch {
    return null;
  }
}

function colorsFromHtml(html) {
  const colors = [];
  const hexRe = /#(?:[0-9a-f]{3}){1,2}\b/gi;
  let match;
  while ((match = hexRe.exec(String(html))) !== null && colors.length < 6) {
    const normalized = normalizeHex(match[0]);
    if (!normalized) continue;
    if (['#ffffff', '#fff', '#000000', '#000'].includes(normalized)) continue;
    if (!colors.includes(normalized)) colors.push(normalized);
  }
  return colors;
}

function normalizeHex(value) {
  const raw = String(value).trim().toLowerCase();
  if (!raw.startsWith('#')) return null;
  if (raw.length === 4) {
    return `#${raw[1]}${raw[1]}${raw[2]}${raw[2]}${raw[3]}${raw[3]}`;
  }
  if (raw.length === 7) return raw;
  return null;
}

function darken(hex, amount = 0.28) {
  const color = normalizeHex(hex) ?? '#2f6f57';
  const r = Math.round(parseInt(color.slice(1, 3), 16) * (1 - amount));
  const g = Math.round(parseInt(color.slice(3, 5), 16) * (1 - amount));
  const b = Math.round(parseInt(color.slice(5, 7), 16) * (1 - amount));
  return `#${[r, g, b].map((n) => n.toString(16).padStart(2, '0')).join('')}`;
}

function splitTitleLines(title, maxLines = 2) {
  const cleaned = String(title || '未命名页面').replace(/\s+/g, ' ').trim();
  if (!cleaned) return ['未命名页面'];
  if (cleaned.includes('|')) {
    return cleaned
      .split('|')
      .map((part) => part.trim())
      .filter(Boolean)
      .slice(0, maxLines);
  }
  if (cleaned.length <= 16) return [cleaned];
  const midpoint = Math.ceil(cleaned.length / 2);
  const splitAt =
    cleaned.lastIndexOf(' ', midpoint) > 8 ? cleaned.lastIndexOf(' ', midpoint) : midpoint;
  return [cleaned.slice(0, splitAt).trim(), cleaned.slice(splitAt).trim()].filter(Boolean);
}

function lighten(hex, amount = 0.22) {
  const color = normalizeHex(hex) ?? '#2f6f57';
  const r = Math.min(255, Math.round(parseInt(color.slice(1, 3), 16) + 255 * amount));
  const g = Math.min(255, Math.round(parseInt(color.slice(3, 5), 16) + 255 * amount));
  const b = Math.min(255, Math.round(parseInt(color.slice(5, 7), 16) + 255 * amount));
  return `#${[r, g, b].map((n) => n.toString(16).padStart(2, '0')).join('')}`;
}

function inferTagFromContent(html, title = '') {
  const text = `${title}\n${String(html).slice(0, 8000)}`;
  if (/旅行|旅游|攻略|travel/i.test(text)) return '旅行';
  if (/美食|餐厅|菜谱|料理|food/i.test(text)) return '美食';
  if (/报告|分析|研报|数据|report/i.test(text)) return '报告';
  if (/普拉提|瑜伽|健身|运动|pilates|yoga/i.test(text)) return '运动';
  if (/618|促销|活动|优惠|限时|大促/i.test(text)) return '活动';
  return null;
}

export function shouldUseScenicBackground(signals) {
  return /旅行|travel|美食|food|餐|报告|report|分析/i.test(String(signals.tag ?? ''));
}

function resolvePhotoPalette(signals) {
  const tag = String(signals.tag ?? '');
  const accent = signals.accent;
  const accent2 = signals.accent2;

  if (/旅行|travel/i.test(tag)) {
    return {
      sky: '#4f8fb8',
      glow: '#f6d7a8',
      horizon: '#e39a4d',
      land: '#24343a',
      shadow: '#0d1518',
      flare: '#ffe9c7',
      bokeh: '#fff8ef',
    };
  }
  if (/美食|food|餐/i.test(tag)) {
    return {
      sky: '#5a2b22',
      glow: '#ffb27a',
      horizon: '#d85f3b',
      land: '#241412',
      shadow: '#120909',
      flare: '#ffd0a8',
      bokeh: '#ffe8d6',
    };
  }
  if (/报告|report|分析/i.test(tag)) {
    return {
      sky: '#3d4f68',
      glow: '#9eb4d8',
      horizon: '#607892',
      land: '#1a2430',
      shadow: '#0a1018',
      flare: '#c8d8ef',
      bokeh: '#eef3fb',
    };
  }

  return {
    sky: lighten(accent, 0.18),
    glow: lighten(accent, 0.34),
    horizon: accent,
    land: darken(accent2, 0.1),
    shadow: darken(accent2, 0.32),
    flare: lighten(accent, 0.42),
    bokeh: '#fff8f2',
  };
}

export function extractCoverSignals(html, meta = {}) {
  const coverMeta = parseCoverMeta(html);
  const rawTitle = meta.title || h1FromHtml(html) || titleFromHtml(html) || '未命名页面';
  const title = rawTitle.replace(/\p{Extended_Pictographic}/gu, '').replace(/\s+/g, ' ').trim();
  const colors = colorsFromHtml(html);
  const accent = normalizeHex(coverMeta.accent ?? meta.accent ?? colors[0] ?? '#2f6f57');
  const accent2 = normalizeHex(coverMeta.accent2 ?? colors[1] ?? darken(accent, 0.15));
  return {
    title: title || '未命名页面',
    subtitle:
      coverMeta.subtitle ??
      meta.subtitle ??
      descriptionFromHtml(html) ??
      'TKMind 作品',
    tag:
      coverMeta.tag ??
      meta.tag ??
      inferTagFromContent(html, title) ??
      '精选页面',
    emoji: coverMeta.emoji ?? meta.emoji ?? extractEmoji(rawTitle) ?? extractEmoji(h1FromHtml(html)),
    accent,
    accent2,
    mood: coverMeta.mood ?? meta.mood ?? 'photo',
    image:
      coverMeta.cover ??
      coverMeta.image ??
      meta.cover ??
      meta.image ??
      coverImageFromHtml(html),
  };
}

function themeBackgroundLayers(signals) {
  const accent = escapeXml(signals.accent);
  const accent2 = escapeXml(signals.accent2);
  const glow = escapeXml(lighten(signals.accent, 0.28));
  return `<rect width="${FEED_WIDTH}" height="${FEED_HEIGHT}" fill="url(#theme)"/>
  <rect width="${FEED_WIDTH}" height="${FEED_HEIGHT}" fill="url(#themeGlow)"/>
  <rect width="${FEED_WIDTH}" height="${FEED_HEIGHT}" fill="url(#themeShade)" opacity="0.72"/>`;
}

export function buildFeedThumbnailSvg(signals, options = {}) {
  const coverDataUri = options.coverDataUri ?? null;
  const hasPhoto = Boolean(coverDataUri);
  const useScenic = !hasPhoto && shouldUseScenicBackground(signals);
  const palette = resolvePhotoPalette(signals);
  const titleLines = splitTitleLines(signals.title);
  const line1 = escapeXml(titleLines[0] ?? signals.title).slice(0, 24);
  const line2 = escapeXml(titleLines[1] ?? '').slice(0, 24);
  const subtitle = escapeXml(signals.subtitle).slice(0, 42);
  const tag = escapeXml(signals.tag).slice(0, 12);
  const sky = escapeXml(palette.sky);
  const glow = escapeXml(palette.glow);
  const horizon = escapeXml(palette.horizon);
  const land = escapeXml(palette.land);
  const shadow = escapeXml(palette.shadow);
  const flare = escapeXml(palette.flare);
  const bokeh = escapeXml(palette.bokeh);
  const titleY2 = line2 ? 652 : 0;
  const scenicLayers = useScenic
    ? `<rect width="${FEED_WIDTH}" height="${FEED_HEIGHT}" fill="url(#sky)"/>
  <rect width="${FEED_WIDTH}" height="${FEED_HEIGHT}" fill="url(#sun)"/>
  <circle cx="118" cy="168" r="58" fill="${bokeh}" opacity="0.16" filter="url(#blurSoft)"/>
  <circle cx="430" cy="248" r="42" fill="${bokeh}" opacity="0.2" filter="url(#blurTight)"/>
  <circle cx="360" cy="118" r="26" fill="${flare}" opacity="0.34" filter="url(#blurTight)"/>
  <circle cx="72" cy="320" r="18" fill="${bokeh}" opacity="0.28" filter="url(#blurTight)"/>
  <path d="M0 470 L90 404 L180 438 L286 372 L392 418 L500 360 L540 388 L540 720 L0 720 Z" fill="${land}" opacity="0.92"/>
  <path d="M0 520 L120 468 L250 502 L360 456 L470 492 L540 460 L540 720 L0 720 Z" fill="${shadow}" opacity="0.55"/>`
    : !hasPhoto
      ? themeBackgroundLayers(signals)
      : '';
  const photoLayer = hasPhoto
    ? `<image href="${svgImageHref(coverDataUri)}" x="0" y="0" width="${FEED_WIDTH}" height="${FEED_HEIGHT}" preserveAspectRatio="xMidYMid slice"/>`
    : '';
  const overlayStops = hasPhoto
    ? `<stop offset="0%" stop-color="#000000" stop-opacity="0.12"/>
      <stop offset="45%" stop-color="#000000" stop-opacity="0.02"/>
      <stop offset="100%" stop-color="${shadow}" stop-opacity="0.88"/>`
    : `<stop offset="0%" stop-color="#000000" stop-opacity="0"/>
      <stop offset="52%" stop-color="#000000" stop-opacity="0.08"/>
      <stop offset="100%" stop-color="${shadow}" stop-opacity="0.82"/>`;
  const vignetteOpacity = hasPhoto ? '0.52' : '0.42';
  const grainOpacity = hasPhoto ? '0.18' : '0.28';

  return `<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="${FEED_WIDTH}" height="${FEED_HEIGHT}" viewBox="0 0 ${FEED_WIDTH} ${FEED_HEIGHT}">
  <defs>
    <linearGradient id="theme" x1="18%" y1="0%" x2="82%" y2="100%">
      <stop offset="0%" stop-color="${escapeXml(lighten(signals.accent, 0.12))}"/>
      <stop offset="48%" stop-color="${escapeXml(signals.accent)}"/>
      <stop offset="100%" stop-color="${escapeXml(signals.accent2)}"/>
    </linearGradient>
    <radialGradient id="themeGlow" cx="72%" cy="16%" r="42%">
      <stop offset="0%" stop-color="${escapeXml(lighten(signals.accent, 0.42))}" stop-opacity="0.55"/>
      <stop offset="100%" stop-color="${escapeXml(signals.accent2)}" stop-opacity="0"/>
    </radialGradient>
    <linearGradient id="themeShade" x1="50%" y1="55%" x2="50%" y2="100%">
      <stop offset="0%" stop-color="#000000" stop-opacity="0"/>
      <stop offset="100%" stop-color="#000000" stop-opacity="0.72"/>
    </linearGradient>
    <linearGradient id="sky" x1="50%" y1="0%" x2="50%" y2="100%">
      <stop offset="0%" stop-color="${sky}"/>
      <stop offset="42%" stop-color="${glow}" stop-opacity="0.88"/>
      <stop offset="68%" stop-color="${horizon}"/>
      <stop offset="100%" stop-color="${land}"/>
    </linearGradient>
    <linearGradient id="overlay" x1="50%" y1="0%" x2="50%" y2="100%">
      ${overlayStops}
    </linearGradient>
    <radialGradient id="sun" cx="78%" cy="18%" r="34%">
      <stop offset="0%" stop-color="${flare}" stop-opacity="0.95"/>
      <stop offset="55%" stop-color="${horizon}" stop-opacity="0.24"/>
      <stop offset="100%" stop-color="${horizon}" stop-opacity="0"/>
    </radialGradient>
    <radialGradient id="vignette" cx="50%" cy="48%" r="72%">
      <stop offset="55%" stop-color="#000000" stop-opacity="0"/>
      <stop offset="100%" stop-color="#000000" stop-opacity="0.42"/>
    </radialGradient>
    <filter id="blurSoft" x="-30%" y="-30%" width="160%" height="160%">
      <feGaussianBlur stdDeviation="24"/>
    </filter>
    <filter id="blurTight" x="-40%" y="-40%" width="180%" height="180%">
      <feGaussianBlur stdDeviation="10"/>
    </filter>
    <filter id="grain" x="0%" y="0%" width="100%" height="100%">
      <feTurbulence type="fractalNoise" baseFrequency="0.85" numOctaves="3" stitchTiles="stitch"/>
      <feColorMatrix type="matrix" values="0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 0.14 0"/>
    </filter>
  </defs>
  ${photoLayer}
  ${scenicLayers}
  <rect width="${FEED_WIDTH}" height="${FEED_HEIGHT}" fill="url(#overlay)"/>
  <rect width="${FEED_WIDTH}" height="${FEED_HEIGHT}" fill="url(#vignette)" opacity="${vignetteOpacity}"/>
  <rect width="${FEED_WIDTH}" height="${FEED_HEIGHT}" filter="url(#grain)" opacity="${grainOpacity}"/>
  <text x="34" y="42" fill="rgba(255,255,255,0.72)" font-family="ui-sans-serif, system-ui, sans-serif" font-size="11" font-weight="700" letter-spacing="3.2">${tag.toUpperCase()}</text>
  <text x="34" y="${line2 ? 612 : 636}" fill="#ffffff" font-family="Georgia, 'Times New Roman', serif" font-size="${line2 ? 34 : 38}" font-weight="600" letter-spacing="-0.4">${line1}</text>
  ${line2 ? `<text x="34" y="${titleY2}" fill="rgba(255,255,255,0.94)" font-family="Georgia, 'Times New Roman', serif" font-size="28" font-weight="500">${line2}</text>` : ''}
  <text x="34" y="${line2 ? 686 : 672}" fill="rgba(255,255,255,0.72)" font-family="ui-sans-serif, system-ui, sans-serif" font-size="14" letter-spacing="0.2">${subtitle}</text>
  <text x="506" y="698" text-anchor="end" fill="rgba(255,255,255,0.34)" font-family="ui-sans-serif, system-ui, sans-serif" font-size="10" letter-spacing="2.8">TKMIND</text>
</svg>`;
}

/** @deprecated use buildFeedThumbnailSvg */
export function buildThumbnailSvg({ title, subtitle, accent = '#2f6f57' }) {
  return buildFeedThumbnailSvg({
    title,
    subtitle,
    tag: '页面',
    emoji: '✦',
    accent,
    accent2: darken(accent, 0.15),
    mood: 'legacy',
  });
}

export function assetThumbnailKey(userId, assetId) {
  return path.posix.join('users', userId, 'assets', assetId, 'thumbnail.svg');
}

export function pageThumbnailKey(userId, pageId) {
  return path.posix.join('users', userId, 'pages', pageId, 'thumbnail.svg');
}

export async function writeThumbnail(storageRoot, storageKey, svg) {
  const target = path.resolve(storageRoot, storageKey);
  const root = path.resolve(storageRoot);
  if (target !== root && !target.startsWith(`${root}${path.sep}`)) {
    throw new Error('缩略图路径越界');
  }
  await fs.mkdir(path.dirname(target), { recursive: true });
  await fs.writeFile(target, svg, 'utf8');
  return target;
}

export function isModernFeedThumbnail(svg) {
  if (!svg) return false;
  return /width="540" height="720"/.test(svg) && /filter id="grain"/.test(svg);
}

export async function generateHtmlThumbnail(storageRoot, storageKey, html, meta = {}) {
  const signals = extractCoverSignals(html, meta);
  const coverDataUri = await resolveCoverDataUri({
    storageRoot,
    contentStorageKey: meta.contentStorageKey,
    contentBaseDir: meta.contentBaseDir,
    imageUrl: signals.image,
  });
  const svg = buildFeedThumbnailSvg(signals, { coverDataUri });
  await writeThumbnail(storageRoot, storageKey, svg);
  return svg;
}

export async function ensureHtmlThumbnail(storageRoot, storageKey, html, meta = {}) {
  const existing = await readThumbnailIfExists(storageRoot, storageKey);
  if (existing && isModernFeedThumbnail(existing) && !meta.force) {
    return existing;
  }
  return generateHtmlThumbnail(storageRoot, storageKey, html, meta);
}

export async function ensurePageThumbnail({
  storageRoot,
  pageThumbnailStorageKey,
  html,
  meta = {},
  workspacePublishDir = null,
  workspaceHtmlRelativePath = null,
}) {
  if (workspacePublishDir && workspaceHtmlRelativePath) {
    const sidecar = await readThumbnailIfExists(
      workspacePublishDir,
      workspaceThumbnailRelativePath(workspaceHtmlRelativePath),
    );
    if (sidecar && isModernFeedThumbnail(sidecar)) {
      await writeThumbnail(storageRoot, pageThumbnailStorageKey, sidecar);
      return sidecar;
    }
  }
  return ensureHtmlThumbnail(storageRoot, pageThumbnailStorageKey, html, meta);
}

export async function readThumbnailIfExists(storageRoot, storageKey) {
  try {
    return await fs.readFile(path.resolve(storageRoot, storageKey), 'utf8');
  } catch {
    return null;
  }
}

export function scheduleHtmlThumbnail(storageRoot, storageKey, html, meta = {}) {
  queueMicrotask(() => {
    void ensureHtmlThumbnail(storageRoot, storageKey, html, meta).catch(() => {});
  });
}
