import fs from "node:fs/promises";
import path from "node:path";
import { parse as parseFont } from "opentype.js";
import { fontStorageKey } from "@/db/generator-fonts";
import { env } from "@/lib/runtime-env";

export type ServerProductionProject = Record<string, string | number | boolean | null>;

export type ServerProductionSvg = {
  svg: string;
  sourceMode: "uploaded-vector" | "generated-text";
  warning: string;
};

const fontFiles: Record<string, string> = {
  grotesk: "montserrat-700.woff",
  rounded: "montserrat-700.woff",
  montserrat: "montserrat-700.woff",
  condensed: "bebas-neue-400.woff",
  bebas: "bebas-neue-400.woff",
  oswald: "oswald-600.woff",
  serif: "playfair-display-700.woff",
  playfair: "playfair-display-700.woff",
  pacifico: "pacifico-400.woff",
  dancing: "dancing-script-600.woff",
  allura: "allura-400.woff",
  sacramento: "sacramento-400.woff",
  satisfy: "satisfy-400.woff",
};

export async function createServerProductionSvg(
  project: ServerProductionProject,
  options: { uploadedSvg?: string; rasterSource?: boolean; invalidVectorSource?: boolean } = {},
): Promise<ServerProductionSvg> {
  const widthMm = dimension(project.width_cm);
  const heightMm = dimension(project.height_cm);
  const uploaded = options.uploadedSvg ? sanitizeUploadedSvg(options.uploadedSvg, widthMm, heightMm) : "";
  const invalidUploadedVector = Boolean(options.uploadedSvg) && !uploaded;
  if (uploaded) {
    return {
      svg: svgEnvelope(widthMm, heightMm, uploaded, project, "uploaded-vector"),
      sourceMode: "uploaded-vector",
      warning: "",
    };
  }

  const font = await loadFont(String(project.font || (project.neon_text ? "pacifico" : "montserrat")));
  const text = String(project.sign_text || project.neon_text || "PROJET");
  const glyphs = font.stringToGlyphs(text);
  const gapMm = project.neon_text ? 0 : Math.max(0, Number(project.letter_spacing_cm ?? 4) * 10);
  const availableMm = Math.max(widthMm * .2, widthMm - gapMm * Math.max(0, glyphs.length - 1));
  const totalAdvance = glyphs.reduce((sum, glyph) => sum + (glyph.advanceWidth || font.unitsPerEm), 0);
  const scaleX = availableMm / Math.max(1, totalAdvance);
  const scaleY = heightMm / Math.max(1, font.ascender - font.descender);
  const baseline = font.ascender * scaleY;
  let cursor = 0;
  const paths = glyphs.map((glyph, index) => {
    const glyphPath = glyph.getPath(0, 0, font.unitsPerEm).toPathData(3);
    const node = `<path d="${glyphPath}" transform="translate(${cursor.toFixed(3)} ${baseline.toFixed(3)}) scale(${scaleX.toFixed(6)} ${scaleY.toFixed(6)})" />`;
    cursor += (glyph.advanceWidth || font.unitsPerEm) * scaleX + (index < glyphs.length - 1 ? gapMm : 0);
    return node;
  }).join("");
  const groupId = project.neon_text ? "NEON_CONTOURS" : "CUT_CONTOURS";
  const color = String(project.color || project.sign_color || "#111111");
  const warning = options.rasterSource
    ? "Source PNG/JPG non vectorielle : contours reconstruits depuis le texte configuré, à contrôler avant fabrication."
    : options.invalidVectorSource || invalidUploadedVector
      ? "Source SVG invalide ou sans contours exploitables : tracé reconstruit depuis le texte configuré, à contrôler avant fabrication."
      : "";
  return {
    svg: svgEnvelope(
      widthMm,
      heightMm,
      `<g id="${groupId}" fill="${escapeAttribute(color)}">${paths}</g>`,
      project,
      "generated-text",
      warning,
    ),
    sourceMode: "generated-text",
    warning,
  };
}

async function loadFont(code: string) {
  const stored = await fontStorageKey(code);
  if (stored) {
    const object = await env.MEDIA.get(stored.key);
    if (object) return parseFont(object.body);
  }
  const fileName = fontFiles[code] ?? fontFiles.montserrat;
  const bytes = await fs.readFile(path.join(process.cwd(), "public", "fonts", fileName));
  const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
  return parseFont(buffer);
}

function sanitizeUploadedSvg(source: string, widthMm: number, heightMm: number) {
  if (!/<svg[\s>]/i.test(source) || !/<\/svg>/i.test(source)) return "";
  const opening = source.match(/<svg\b([^>]*)>/i);
  const content = source.match(/<svg\b[^>]*>([\s\S]*?)<\/svg>/i)?.[1] ?? "";
  if (!opening || !content) return "";
  const attributes = opening[1];
  const viewBox = attribute(attributes, "viewBox")
    || `0 0 ${numericAttribute(attributes, "width", 1000)} ${numericAttribute(attributes, "height", 400)}`;
  const cleaned = content
    .replace(/<\?xml[\s\S]*?\?>/gi, "")
    .replace(/<!DOCTYPE[\s\S]*?>/gi, "")
    .replace(/<(script|style|foreignObject|iframe|object|embed|animate|set|image)\b[\s\S]*?<\/\1>/gi, "")
    .replace(/<(script|style|foreignObject|iframe|object|embed|animate|set|image)\b[^>]*\/?>/gi, "")
    .replace(/\s+on[a-z]+\s*=\s*(?:"[^"]*"|'[^']*')/gi, "")
    .replace(/\s+style\s*=\s*(?:"[^"]*"|'[^']*')/gi, "")
    .replace(/\s+(?:href|xlink:href)\s*=\s*(?:"(?:https?:|javascript:)[^"]*"|'(?:https?:|javascript:)[^']*')/gi, "");
  if (!/<(?:path|polygon|polyline|rect|circle|ellipse|line|text|use)\b/i.test(cleaned)) return "";
  return `<svg x="0" y="0" width="${widthMm}" height="${heightMm}" viewBox="${escapeAttribute(viewBox)}" preserveAspectRatio="xMidYMid meet">${cleaned}</svg>`;
}

function svgEnvelope(
  widthMm: number,
  heightMm: number,
  body: string,
  project: ServerProductionProject,
  sourceMode: ServerProductionSvg["sourceMode"],
  warning = "",
) {
  return `<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="${widthMm}mm" height="${heightMm}mm" viewBox="0 0 ${widthMm} ${heightMm}" preserveAspectRatio="xMidYMid meet" data-source-mode="${sourceMode}">
  <title>${escapeXml(String(project.reference))} · ${escapeXml(String(project.sign_text || project.neon_text || "Projet"))}</title>
  <metadata>Échelle 1:1 · ${widthMm} × ${heightMm} mm · ${project.neon_text ? `néon flex ${escapeXml(String(project.tube_mm))} mm` : `profil ${escapeXml(String(project.profile_code))}`}${warning ? ` · ${escapeXml(warning)}` : ""}</metadata>
  ${body}
</svg>`;
}

function dimension(value: unknown) {
  return Math.max(10, Math.round(Number(value) * 10));
}

function attribute(source: string, name: string) {
  return source.match(new RegExp(`\\b${name}\\s*=\\s*["']([^"']+)["']`, "i"))?.[1] ?? "";
}

function numericAttribute(source: string, name: string, fallback: number) {
  const value = Number.parseFloat(attribute(source, name));
  return Number.isFinite(value) && value > 0 ? value : fallback;
}

function escapeXml(value: string) {
  return value.replace(/[<>&'"]/g, (character) => ({ "<": "&lt;", ">": "&gt;", "&": "&amp;", "'": "&apos;", '"': "&quot;" })[character] ?? character);
}

function escapeAttribute(value: string) {
  return escapeXml(value);
}
