import type { SubsonicAlbum } from '../../api/subsonicTypes';
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { coverArtRef } from '../../cover/ref';
import { loadCoverBlobForExport } from '../../cover/integrations/export';
import PsysonicLogo from '../../components/PsysonicLogo';

export type ExportFormat = 'story' | 'square' | 'twitter';
export type ExportGridSize = 3 | 4 | 5;

export interface ExportAlbumCardOptions {
  albums: SubsonicAlbum[];
  format: ExportFormat;
  gridSize: ExportGridSize;
  /** Footer label like "Top Albums". */
  title: string;
  /** Footer secondary label like the period or "Most Played". */
  periodLabel?: string;
  /** Footer-right text shown next to the wordmark, usually a period or count. */
  meta?: string;
  /** Optional explicit accent override; defaults to the document's `--accent`. */
  accent?: string;
  /** Optional explicit background override; defaults to the document's `--bg-primary`. */
  background?: string;
  /** Set to true while rendering a low-res preview. Skips slow font/quality settings. */
  preview?: boolean;
}

const DIMENSIONS: Record<ExportFormat, { w: number; h: number }> = {
  story:   { w: 1080, h: 1920 },
  square:  { w: 1080, h: 1080 },
  twitter: { w: 1200, h: 675 },
};

// Preview canvas resolution. 540 left text and album covers visibly upscaled
// (and so blurry) when CSS stretched the canvas back up to the modal width
// — match the full export width for Square/Story (1080) so the preview is
// pixel-crisp at any modal size, and only Twitter scales down (1200 → 1080).
const PREVIEW_MAX_WIDTH = 1080;

/** Reads a `--var` from `document.documentElement`, with optional fallback. */
function readThemeVar(name: string, fallback: string): string {
  const v = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
  return v || fallback;
}

/** Quick contrast check — returns true if the background is light. */
function isLight(hex: string): boolean {
  // Accepts `#RRGGBB`, `#RGB`, or `rgb(r, g, b)` — fall back to dark.
  const m = hex.match(/^#([0-9a-f]{6})$/i) || hex.match(/^#([0-9a-f]{3})$/i);
  if (!m) {
    const rgb = hex.match(/rgb\(\s*(\d+)\D+(\d+)\D+(\d+)/i);
    if (rgb) return (Number(rgb[1]) + Number(rgb[2]) + Number(rgb[3])) / 3 > 160;
    return false;
  }
  const v = m[1];
  const expand = v.length === 3 ? v.split('').map(c => c + c).join('') : v;
  const r = parseInt(expand.slice(0, 2), 16);
  const g = parseInt(expand.slice(2, 4), 16);
  const b = parseInt(expand.slice(4, 6), 16);
  return (r + g + b) / 3 > 160;
}

async function loadAlbumCover(album: SubsonicAlbum, displayCssPx: number, signal?: AbortSignal): Promise<ImageBitmap | null> {
  if (!album.coverArt) return null;
  try {
    const blob = await loadCoverBlobForExport(coverArtRef(album.coverArt), displayCssPx, signal);
    if (!blob) return null;
    return await createImageBitmap(blob);
  } catch {
    return null;
  }
}

/** Decodes the Psysonic wordmark SVG into an Image, ready for drawImage.
 *  `targetHeight` is informational — actual scaling happens at drawImage time. */
async function loadWordmark(color: string): Promise<HTMLImageElement> {
  const svgMarkup = getCachedLogoSvg(color);
  const blob = new Blob([svgMarkup], { type: 'image/svg+xml' });
  const url = URL.createObjectURL(blob);
  try {
    const img = new Image();
    img.decoding = 'async';
    img.src = url;
    await img.decode();
    return img;
  } finally {
    setTimeout(() => URL.revokeObjectURL(url), 0);
  }
}

/** Renders the PsysonicLogo component to an SVG string with a single solid
 *  color baked in (both gradient stops set to the same value), so the wordmark
 *  stays uniformly readable on the export background regardless of theme.
 *  The naive `var(--logo-color-start)` regex misses the nested fallback
 *  `var(--logo-color-start, var(--accent))` — we use exact-string replace. */
let cachedLogoSvgKey = '';
let cachedLogoSvg = '';
function getCachedLogoSvg(color: string): string {
  if (cachedLogoSvgKey === color && cachedLogoSvg) return cachedLogoSvg;
  const raw = renderToStaticMarkup(React.createElement(PsysonicLogo, { gradientIdSuffix: 'export' }));
  const swapped = raw
    .replace('var(--logo-color-start, var(--accent))', color)
    .replace('var(--logo-color-end, var(--accent-2))', color);
  cachedLogoSvg = swapped;
  cachedLogoSvgKey = color;
  return swapped;
}

/**
 * Renders a Pano-Scrobbler-style "Top Albums" image to a canvas and returns it
 * as a Blob (PNG). Caller is responsible for saving the blob to disk.
 *
 * The function reads accent + background colors from the active theme so the
 * exported image matches the in-app look. Cover art is loaded through the
 * existing IndexedDB cache (`getCachedBlob`), so repeated exports are cheap.
 */
export async function renderAlbumCardCanvas(opts: ExportAlbumCardOptions): Promise<HTMLCanvasElement> {
  const { albums, format, gridSize, preview } = opts;
  const dims = DIMENSIONS[format];
  const scale = preview ? Math.min(1, PREVIEW_MAX_WIDTH / dims.w) : 1;
  const w = Math.round(dims.w * scale);
  const h = Math.round(dims.h * scale);

  const accent = opts.accent ?? readThemeVar('--accent', '#CBA6F7');
  const bg = opts.background ?? readThemeVar('--bg-primary', '#1E1E2E');
  const fgPrimary = readThemeVar('--text-primary', isLight(bg) ? '#11111B' : '#CDD6F4');
  const fgMuted = readThemeVar('--text-muted', isLight(bg) ? '#444' : '#9399B2');

  const canvas = document.createElement('canvas');
  canvas.width = w;
  canvas.height = h;
  const ctx = canvas.getContext('2d');
  if (!ctx) throw new Error('canvas 2d unavailable');

  // ── Background ─────────────────────────────────────────────────────────
  // Subtle vertical gradient: bg → bg with a slight accent tint at the bottom.
  const bgGrad = ctx.createLinearGradient(0, 0, 0, h);
  bgGrad.addColorStop(0, bg);
  bgGrad.addColorStop(1, mixHex(bg, accent, 0.12));
  ctx.fillStyle = bgGrad;
  ctx.fillRect(0, 0, w, h);

  // ── Layout ───────────────────────────────────────────────────────────────
  // Header / footer have format-specific minimum heights so the title and
  // logo never collide visually with the grid even when the grid is
  // height-bounded (Square, Twitter). Landscape (Twitter) needs the largest
  // proportional band because the card is short. Portrait (Story) the
  // smallest because there's already plenty of vertical room.
  const pad = Math.round(w * 0.045);
  const gap = Math.round(w * 0.012);
  const isLandscape = format === 'twitter';
  const isPortrait = format === 'story';
  // Story stacks logo above the meta-label, so it needs the most header room.
  const headerMinRatio = isPortrait ? 0.16 : isLandscape ? 0.18 : 0.13;
  // Square + Story have a URL footer; Twitter doesn't (URL lives in header).
  const footerMinRatio = isLandscape ? 0.05 : isPortrait ? 0.07 : 0.08;
  const headerMin = Math.round(h * headerMinRatio);
  const footerMin = Math.round(h * footerMinRatio);
  let headerH = headerMin;
  let footerH = footerMin;
  const horizontalTile = Math.floor((w - pad * 2 - gap * (gridSize - 1)) / gridSize);
  let availableH = h - headerH - footerH;
  let verticalTile = Math.floor((availableH - gap * (gridSize - 1)) / gridSize);
  let tileSize = Math.min(horizontalTile, verticalTile);
  // If we're width-bounded the grid leaves vertical slack — push header
  // (60%) and footer (40%) outward to absorb it. Header/footer content
  // stays anchored away from the grid edge so it can't drift in.
  const gridPxH0 = tileSize * gridSize + gap * (gridSize - 1);
  const verticalSlack = availableH - gridPxH0;
  if (verticalSlack > 0) {
    headerH += Math.round(verticalSlack * 0.6);
    footerH += verticalSlack - Math.round(verticalSlack * 0.6);
    availableH = h - headerH - footerH;
    verticalTile = Math.floor((availableH - gap * (gridSize - 1)) / gridSize);
    tileSize = Math.min(horizontalTile, verticalTile);
  }
  const gridPxW = tileSize * gridSize + gap * (gridSize - 1);
  const gridFinalH = tileSize * gridSize + gap * (gridSize - 1);
  const gridX = pad + Math.round((w - pad * 2 - gridPxW) / 2);
  const gridY = headerH + Math.round((h - headerH - footerH - gridFinalH) / 2);
  const headerHasSlack = verticalSlack > 0;

  // ── Header: format-specific ──────────────────────────────────────────────
  // Story (portrait):   logo centered, meta-label centered below it.
  // Twitter (landscape): logo left,    meta center,         url right (small).
  // Square:              logo left,    meta right.
  const logo = await loadWordmark(accent).catch(() => null);
  const logoRatio = logo ? (logo.naturalWidth / logo.naturalHeight || 4.4) : 4.4;
  const drawLogoAt = (lx: number, ly: number, lh: number) => {
    if (logo) {
      ctx.drawImage(logo, lx, ly, Math.round(lh * logoRatio), lh);
    } else {
      ctx.fillStyle = accent;
      ctx.font = `700 ${Math.round(lh * 0.78)}px "Space Grotesk", "Inter", system-ui, sans-serif`;
      ctx.textAlign = 'left';
      ctx.textBaseline = 'top';
      ctx.fillText('psysonic', lx, ly);
    }
  };

  // Header label is hardcoded English ("Top Albums") so a shared image is
  // legible to anyone, regardless of the exporter's UI language. Caller can
  // still override via `opts.meta` for special editions ("Top Albums 2026").
  const headerLabel = opts.meta ?? 'Top Albums';

  if (isPortrait) {
    // Story: stacked logo + label, both centered.
    const logoH = Math.max(36, Math.round(headerMin * 0.36));
    const logoW = Math.round(logoH * logoRatio);
    const metaSize = Math.max(18, Math.round(headerMin * 0.18));
    const stackGap = Math.round(headerMin * 0.08);
    const totalH = logoH + stackGap + metaSize;
    const stackTop = headerHasSlack
      ? Math.round(pad * 0.85)
      : Math.round((headerMin - totalH) / 2);
    const logoX = Math.round((w - logoW) / 2);
    drawLogoAt(logoX, stackTop, logoH);
    ctx.font = `500 ${metaSize}px "Inter", system-ui, sans-serif`;
    ctx.fillStyle = fgMuted;
    ctx.textAlign = 'center';
    ctx.textBaseline = 'top';
    ctx.fillText(headerLabel, Math.round(w / 2), stackTop + logoH + stackGap);
  } else if (isLandscape) {
    // Twitter: logo left, label centered, url right (smaller).
    const logoH = Math.max(30, Math.round(headerMin * 0.42));
    const headerCenterY = headerHasSlack
      ? Math.round(pad * 0.85) + Math.round(logoH / 2)
      : Math.round(headerMin / 2);
    drawLogoAt(pad, headerCenterY - Math.round(logoH / 2), logoH);
    const metaSize = Math.max(16, Math.round(headerMin * 0.22));
    ctx.font = `600 ${metaSize}px "Inter", system-ui, sans-serif`;
    ctx.fillStyle = fgPrimary;
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText(headerLabel, Math.round(w / 2), headerCenterY);
    const urlSize = Math.max(13, Math.round(headerMin * 0.16));
    ctx.font = `500 ${urlSize}px "Inter", system-ui, sans-serif`;
    ctx.fillStyle = fgMuted;
    ctx.textAlign = 'right';
    ctx.textBaseline = 'middle';
    ctx.fillText('www.psysonic.de', w - pad, headerCenterY);
  } else {
    // Square: logo left, label right.
    const logoH = Math.max(28, Math.round(headerMin * 0.40));
    const headerCenterY = headerHasSlack
      ? Math.round(pad * 0.85) + Math.round(logoH / 2)
      : Math.round(headerMin / 2);
    drawLogoAt(pad, headerCenterY - Math.round(logoH / 2), logoH);
    const metaSize = Math.max(14, Math.round(headerMin * 0.22));
    ctx.font = `500 ${metaSize}px "Inter", system-ui, sans-serif`;
    ctx.fillStyle = fgMuted;
    ctx.textAlign = 'right';
    ctx.textBaseline = 'middle';
    ctx.fillText(headerLabel, w - pad, headerCenterY);
  }

  // ── Tiles ────────────────────────────────────────────────────────────────
  // Match the export tile resolution so preview covers downsample crisply
  // into the (now full-width) canvas instead of upscaling 256 → ~300 and
  // blurring every album thumbnail.
  const desiredTilePx = 600;
  const needed = gridSize * gridSize;
  const tilesAlbums = albums.slice(0, needed);
  const covers = await Promise.all(tilesAlbums.map(a => loadAlbumCover(a, desiredTilePx)));

  for (let i = 0; i < tilesAlbums.length; i++) {
    const album = tilesAlbums[i];
    const col = i % gridSize;
    const row = Math.floor(i / gridSize);
    const x = gridX + col * (tileSize + gap);
    const y = gridY + row * (tileSize + gap);

    // Cover or fallback panel.
    const cover = covers[i];
    if (cover) {
      ctx.drawImage(cover, x, y, tileSize, tileSize);
    } else {
      ctx.fillStyle = mixHex(bg, accent, 0.15);
      ctx.fillRect(x, y, tileSize, tileSize);
    }

    // Info strip — narrow bar at the bottom of the cover with the rank on
    // the left and the play-count on the right. Full-width strip integrates
    // visually into the cover (as opposed to a floating pill).
    const stripH = Math.max(20, Math.round(tileSize * 0.13));
    const stripY = y + tileSize - stripH;
    ctx.fillStyle = 'rgba(0, 0, 0, 0.68)';
    ctx.fillRect(x, stripY, tileSize, stripH);

    const stripFontSize = Math.round(stripH * 0.52);
    const stripPadX = Math.round(stripH * 0.45);
    const stripCenterY = stripY + stripH / 2 + 1;

    // Rank on the left.
    ctx.font = `800 ${stripFontSize}px "Space Grotesk", "Inter", system-ui, sans-serif`;
    ctx.fillStyle = '#fff';
    ctx.textAlign = 'left';
    ctx.textBaseline = 'middle';
    ctx.fillText(String(i + 1), x + stripPadX, stripCenterY);

    // Plays on the right (only when available).
    const plays = album.playCount;
    if (plays && plays > 0) {
      ctx.font = `600 ${stripFontSize}px "Inter", system-ui, sans-serif`;
      ctx.fillStyle = 'rgba(255, 255, 255, 0.88)';
      ctx.textAlign = 'right';
      ctx.fillText(`${plays} Plays`, x + tileSize - stripPadX, stripCenterY);
    }
  }

  // ── Footer: URL centered (Story + Square only; Twitter has it in header) ─
  if (!isLandscape) {
    const urlSize = Math.max(13, Math.round(footerMin * 0.36));
    ctx.font = `500 ${urlSize}px "Inter", system-ui, sans-serif`;
    ctx.fillStyle = fgMuted;
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    const footerCenterY = h - Math.round(footerMin / 2);
    ctx.fillText('www.psysonic.de', Math.round(w / 2), footerCenterY);
  }

  return canvas;
}

export async function exportAlbumCardBlob(opts: ExportAlbumCardOptions): Promise<Blob> {
  const canvas = await renderAlbumCardCanvas(opts);
  return await new Promise<Blob>((resolve, reject) => {
    canvas.toBlob(b => (b ? resolve(b) : reject(new Error('toBlob returned null'))), 'image/png');
  });
}

// ──────────────────────────────────────────────────────────────────────────
// Helpers
// ──────────────────────────────────────────────────────────────────────────

function roundRect(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, r: number) {
  ctx.beginPath();
  ctx.moveTo(x + r, y);
  ctx.lineTo(x + w - r, y);
  ctx.quadraticCurveTo(x + w, y, x + w, y + r);
  ctx.lineTo(x + w, y + h - r);
  ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
  ctx.lineTo(x + r, y + h);
  ctx.quadraticCurveTo(x, y + h, x, y + h - r);
  ctx.lineTo(x, y + r);
  ctx.quadraticCurveTo(x, y, x + r, y);
  ctx.closePath();
}

/** Mixes two hex colors by `t` ∈ [0..1]. Falls back to `a` when parsing fails. */
function mixHex(a: string, b: string, t: number): string {
  const ca = hexToRgb(a);
  const cb = hexToRgb(b);
  if (!ca || !cb) return a;
  const r = Math.round(ca.r + (cb.r - ca.r) * t);
  const g = Math.round(ca.g + (cb.g - ca.g) * t);
  const bl = Math.round(ca.b + (cb.b - ca.b) * t);
  return `rgb(${r}, ${g}, ${bl})`;
}

function hexToRgb(input: string): { r: number; g: number; b: number } | null {
  const trimmed = input.trim();
  const hex = trimmed.match(/^#([0-9a-f]{3,8})$/i);
  if (hex) {
    let v = hex[1];
    if (v.length === 3) v = v.split('').map(c => c + c).join('');
    if (v.length === 6 || v.length === 8) {
      return {
        r: parseInt(v.slice(0, 2), 16),
        g: parseInt(v.slice(2, 4), 16),
        b: parseInt(v.slice(4, 6), 16),
      };
    }
  }
  const rgb = trimmed.match(/rgba?\(\s*(\d+)[,\s]+(\d+)[,\s]+(\d+)/i);
  if (rgb) return { r: Number(rgb[1]), g: Number(rgb[2]), b: Number(rgb[3]) };
  return null;
}
