import {
  useEffect,
  useState,
  useRef,
  type ChangeEvent as ReactChangeEvent,
  type DragEvent,
  type KeyboardEvent as ReactKeyboardEvent,
  type MouseEvent as ReactMouseEvent,
} from "react";
import { ApiError, apiGet, apiPut } from "../lib/api";
import { useSession } from "../lib/session";
import type { UserOption } from "../lib/types";

const CANVAS_WIDTH = 794;
const CANVAS_HEIGHT = 1123;
const CANVAS_ASPECT_RATIO = CANVAS_WIDTH / CANVAS_HEIGHT;
const GRID_SIZE = 5;
const DRAG_DATA_TYPE = "application/x-muh-invoice-template";
const PDF_PAGE_WIDTH = 595.28;
const PDF_PAGE_HEIGHT = 841.89;
const PDF_SCALE = PDF_PAGE_WIDTH / CANVAS_WIDTH;
const PDF_TEXT_PADDING_X = 12;
const PDF_TEXT_PADDING_Y = 10;
const PDF_LINE_HEIGHT = 1.35;
const CANVAS_BOTTOM_GAP = 10;
const MIN_ELEMENT_WIDTH = 100;
const MIN_MEDIA_WIDTH = 40;
const MIN_MEDIA_HEIGHT = 40;
const MIN_LINE_THICKNESS = 1;
const MIN_LINE_LENGTH = 40;
const MIN_FONT_SIZE = 12;
const MAX_FONT_SIZE = 44;
const INVOICE_LOCKED_TEXT_PALETTE_IDS = new Set([
  "invoice-number",
  "invoice-date",
  "invoice-due-date",
  "customer-number",
  "customer-name",
  "customer-street",
  "customer-house-number",
  "customer-postal-code",
  "customer-city",
  "customer-email",
  "customer-phone",


  "payment-terms",
  "bank-details",
  "issuer-name",
  "issuer-street",
  "issuer-house-number",
  "issuer-postal-code",
  "issuer-city",
  "issuer-contact",
]);

type ElementKind = "text" | "line" | "image";
type LineOrientation = "horizontal" | "vertical";
type TextAlign = "left" | "center" | "right";
type FontWeight = 400 | 500 | 600 | 700;
type PaletteCategory = string;

interface PaletteItem {
  id: string;
  category: PaletteCategory;
  label: string;
  description: string;
  kind?: ElementKind;
  width: number;
  height?: number;
  fontSize: number;
  fontWeight: FontWeight;
  textAlign: TextAlign;
  lineOrientation?: LineOrientation;
  defaultContent: (user: UserOption | null) => string;
}

interface TemplateElement {
  id: string;
  paletteId: string;
  kind: ElementKind;
  label: string;
  content: string;
  x: number;
  y: number;
  width: number;
  height?: number;
  fontSize: number;
  fontWeight: FontWeight;
  textAlign: TextAlign;
  lineOrientation?: LineOrientation;
  imageSrc?: string | null;
  imageNaturalWidth?: number | null;
  imageNaturalHeight?: number | null;
}

interface CanvasViewport {
  height: number;
  scale: number;
  syncPanelHeight: boolean;
  width: number;
}

interface PdfImageResource {
  body: string;
  elementId: string;
  name: string;
  pixelHeight: number;
  pixelWidth: number;
}

interface UploadedImageAsset {
  dataUrl: string;
  height: number;
  width: number;
}

interface InvoiceTemplateResponse {
  elements: unknown;
  stored: boolean;
  updatedAt: string | null;
}

type DragPayload =
  | {
      kind: "palette";
      paletteId: string;
    }
  | {
      kind: "element";
      elementId: string;
    };

const CP1252_MAP: Record<number, number> = {
  338: 140,
  339: 156,
  352: 138,
  353: 154,
  376: 159,
  381: 142,
  382: 158,
  402: 131,
  710: 136,
  732: 152,
  8211: 150,
  8212: 151,
  8216: 145,
  8217: 146,
  8218: 130,
  8220: 147,
  8221: 148,
  8222: 132,
  8224: 134,
  8225: 135,
  8226: 149,
  8230: 133,
  8240: 137,
  8249: 139,
  8250: 155,
  8364: 128,
  8482: 153,
};

const INVOICE_PALETTE_ITEMS: PaletteItem[] = [
  {
    id: "invoice-title",
    category: "invoice-header",
    label: "Rechnungstitel",
    description: "Titel der Rechnung",
    width: 200,
    fontSize: 28,
    fontWeight: 700,
    textAlign: "left",
    defaultContent: () => "Rechnung",
  },
  {
    id: "invoice-number",
    category: "invoice-header",
    label: "Rechnungsnummer",
    description: "Automatische Rechnungsnummer",
    width: 200,
    fontSize: 16,
    fontWeight: 600,
    textAlign: "left",
    defaultContent: () => "Rechnungsnr.: R-2026-001",
  },
  {
    id: "invoice-date",
    category: "invoice-header",
    label: "Rechnungsdatum",
    description: "Datum der Rechnungserstellung",
    width: 200,
    fontSize: 16,
    fontWeight: 400,
    textAlign: "left",
    defaultContent: () => `Datum: ${new Intl.DateTimeFormat("de-DE", { dateStyle: "medium" }).format(new Date())}`,
  },
  {
    id: "invoice-due-date",
    category: "invoice-header",
    label: "Fälligkeitsdatum",
    description: "Zahlungsfälligkeit",
    width: 200,
    fontSize: 16,
    fontWeight: 400,
    textAlign: "left",
    defaultContent: () => "Fällig bis: 15.04.2026",
  },
  {
    id: "customer-number",
    category: "customer-data",
    label: "Kundennummer",
    description: "Kundennummer",
    width: 200,
    fontSize: 14,
    fontWeight: 400,
    textAlign: "left",
    defaultContent: () => "Kunden-Nr.: K-12345",
  },
  {
    id: "customer-name",
    category: "customer-data",
    label: "Kundenname",
    description: "Name des Kunden",
    width: 280,
    fontSize: 16,
    fontWeight: 600,
    textAlign: "left",
    defaultContent: () => "VoS, Dirk Schwissel",
  },
  {
    id: "customer-street",
    category: "customer-data",
    label: "Kunden-Straße",
    description: "Straße des Kunden",
    width: 200,
    fontSize: 14,
    fontWeight: 400,
    textAlign: "left",
    defaultContent: () => "Musterstraße",
  },
  {
    id: "customer-house-number",
    category: "customer-data",
    label: "Kunden-Hausnummer",
    description: "Hausnummer des Kunden",
    width: 80,
    fontSize: 14,
    fontWeight: 400,
    textAlign: "left",
    defaultContent: () => "123",
  },
  {
    id: "customer-postal-code",
    category: "customer-data",
    label: "Kunden-PLZ",
    description: "Postleitzahl des Kunden",
    width: 80,
    fontSize: 14,
    fontWeight: 400,
    textAlign: "left",
    defaultContent: () => "12345",
  },
  {
    id: "customer-city",
    category: "customer-data",
    label: "Kunden-Ort",
    description: "Ort des Kunden",
    width: 200,
    fontSize: 14,
    fontWeight: 400,
    textAlign: "left",
    defaultContent: () => "Musterstadt",
  },
  {
    id: "customer-email",
    category: "customer-data",
    label: "Kunden-E-Mail",
    description: "E-Mail-Adresse des Kunden",
    width: 250,
    fontSize: 14,
    fontWeight: 400,
    textAlign: "left",
    defaultContent: () => "E-Mail: kunde@example.com",
  },
  {
    id: "customer-phone",
    category: "customer-data",
    label: "Kunden-Telefon",
    description: "Telefonnummer des Kunden",
    width: 200,
    fontSize: 14,
    fontWeight: 400,
    textAlign: "left",
    defaultContent: () => "Tel.: +49 123 456789",
  },
  {
    id: "issuer-name",
    category: "issuer-data",
    label: "Aussteller-Name",
    description: "Name des Rechnungsausstellers",
    width: 280,
    fontSize: 16,
    fontWeight: 700,
    textAlign: "left",
    defaultContent: (user) => user?.companyName ?? "Ihr Unternehmen",
  },
  {
    id: "issuer-street",
    category: "issuer-data",
    label: "Aussteller-Straße",
    description: "Straße des Rechnungsausstellers",
    width: 200,
    fontSize: 14,
    fontWeight: 400,
    textAlign: "left",
    defaultContent: (user) => user?.street ?? "",
  },
  {
    id: "issuer-house-number",
    category: "issuer-data",
    label: "Aussteller-Hausnummer",
    description: "Hausnummer des Rechnungsausstellers",
    width: 80,
    fontSize: 14,
    fontWeight: 400,
    textAlign: "left",
    defaultContent: (user) => user?.houseNumber ?? "",
  },
  {
    id: "issuer-postal-code",
    category: "issuer-data",
    label: "Aussteller-PLZ",
    description: "Postleitzahl des Rechnungsausstellers",
    width: 80,
    fontSize: 14,
    fontWeight: 400,
    textAlign: "left",
    defaultContent: (user) => user?.postalCode ?? "",
  },
  {
    id: "issuer-city",
    category: "issuer-data",
    label: "Aussteller-Ort",
    description: "Ort des Rechnungsausstellers",
    width: 200,
    fontSize: 14,
    fontWeight: 400,
    textAlign: "left",
    defaultContent: (user) => user?.city ?? "",
  },
  {
    id: "issuer-contact",
    category: "issuer-data",
    label: "Aussteller-Kontakt",
    description: "Kontaktdaten des Rechnungsausstellers",
    width: 280,
    fontSize: 14,
    fontWeight: 400,
    textAlign: "left",
    defaultContent: (user) =>
      `Tel.: ${user?.phoneNumber ?? ""}\nE-Mail: ${user?.email ?? ""}`,
  },
  {
    id: "payment-terms",
    category: "invoice-footer",
    label: "Zahlungsbedingungen",
    description: "Zahlungsziel und -bedingungen",
    width: 400,
    fontSize: 14,
    fontWeight: 400,
    textAlign: "left",
    defaultContent: () => "Zahlungsbedingungen: Zahlung innerhalb von 14 Tagen ohne Abzug.",
  },
  {
    id: "bank-details",
    category: "invoice-footer",
    label: "Bankverbindung",
    description: "Bankdaten für die Zahlung",
    width: 400,
    fontSize: 14,
    fontWeight: 400,
    textAlign: "left",
    defaultContent: () =>
      "Bankverbindung:\nIBAN: DE12 3456 7890 1234 5678 90\nBIC: ABCDEFGHXXX\nBank: Musterbank",
  },
  {
    id: "free-text",
    category: "free-elements",
    label: "Freitext",
    description: "Beliebig editierbarer Text",
    width: 220,
    fontSize: 16,
    fontWeight: 400,
    textAlign: "left",
    defaultContent: () => "Freitext",
  },
  {
    id: "line",
    category: "free-elements",
    label: "Linie",
    description: "Linie mit umschaltbarer Ausrichtung",
    kind: "line",
    width: 260,
    height: 3,
    fontSize: 16,
    fontWeight: 400,
    textAlign: "left",
    lineOrientation: "horizontal",
    defaultContent: () => "",
  },
  {
    id: "image",
    category: "free-elements",
    label: "Bild",
    description: "Logo, Kopfgrafik oder Gestaltungselement",
    kind: "image",
    width: 180,
    height: 120,
    fontSize: 16,
    fontWeight: 400,
    textAlign: "left",
    defaultContent: () => "Bild",
  },
];

const INVOICE_PALETTE_GROUPS: Array<{ category: PaletteCategory; title: string }> = [
  { category: "invoice-header", title: "Rechnungskopf" },
  { category: "customer-data", title: "Kundendaten" },
  { category: "issuer-data", title: "Aussteller" },

  { category: "invoice-footer", title: "Fußbereich" },
  { category: "free-elements", title: "Freie Elemente" },
];

function clamp(value: number, minimum: number, maximum: number) {
  return Math.min(Math.max(value, minimum), maximum);
}

function snapToGrid(value: number) {
  return Math.round(value / GRID_SIZE) * GRID_SIZE;
}

function formatPdfNumber(value: number) {
  const normalized = Number(value.toFixed(2));
  if (Number.isInteger(normalized)) {
    return String(normalized);
  }
  return normalized.toFixed(2).replace(/0+$/, "").replace(/\.$/, "");
}

function measureTextWidth(text: string, fontSize: number, fontWeight: FontWeight) {
  if (typeof document === "undefined") {
    return text.length * fontSize * 0.56;
  }

  const canvas = document.createElement("canvas");
  const context = canvas.getContext("2d");
  if (!context) {
    return text.length * fontSize * 0.56;
  }

  context.font = `${fontWeight >= 600 ? 700 : 400} ${fontSize}px Arial`;
  return context.measureText(text).width;
}

function splitLongToken(token: string, maxWidth: number, fontSize: number, fontWeight: FontWeight) {
  const segments: string[] = [];
  let currentSegment = "";

  for (const character of token) {
    const candidate = `${currentSegment}${character}`;
    if (currentSegment && measureTextWidth(candidate, fontSize, fontWeight) > maxWidth) {
      segments.push(currentSegment);
      currentSegment = character;
      continue;
    }
    currentSegment = candidate;
  }

  if (currentSegment) {
    segments.push(currentSegment);
  }

  return segments;
}

function wrapTextLines(text: string, maxWidth: number, fontSize: number, fontWeight: FontWeight) {
  const paragraphs = text.split(/\r?\n/);
  const wrappedLines: string[] = [];

  for (const paragraph of paragraphs) {
    const words = paragraph.trim().split(/\s+/).filter(Boolean);
    if (!words.length) {
      wrappedLines.push("");
      continue;
    }

    let currentLine = "";
    for (const word of words) {
      const fragments =
        measureTextWidth(word, fontSize, fontWeight) > maxWidth
          ? splitLongToken(word, maxWidth, fontSize, fontWeight)
          : [word];

      for (const fragment of fragments) {
        const candidate = currentLine ? `${currentLine} ${fragment}` : fragment;
        if (!currentLine || measureTextWidth(candidate, fontSize, fontWeight) <= maxWidth) {
          currentLine = candidate;
          continue;
        }

        wrappedLines.push(currentLine);
        currentLine = fragment;
      }
    }

    if (currentLine) {
      wrappedLines.push(currentLine);
    }
  }

  return wrappedLines.length ? wrappedLines : [""];
}

function encodePdfText(text: string) {
  const bytes: number[] = [];

  for (const character of text) {
    const codePoint = character.codePointAt(0) ?? 63;
    if (CP1252_MAP[codePoint]) {
      bytes.push(CP1252_MAP[codePoint]);
      continue;
    }
    if (codePoint >= 0 && codePoint <= 255) {
      bytes.push(codePoint);
      continue;
    }
    bytes.push(63);
  }

  return bytes.map((value) => value.toString(16).padStart(2, "0")).join("").toUpperCase();
}

function getDefaultElementHeight(
  kind: ElementKind,
  fontSize: number,
  lineOrientation?: LineOrientation,
) {
  if (kind === "line") {
    return lineOrientation === "vertical" ? 180 : 3;
  }
  if (kind === "image") {
    return 120;
  }
  return Math.max(fontSize + 16, 28);
}

function getMinimumWidthForElement(kind: ElementKind, lineOrientation?: LineOrientation) {
  if (kind === "line") {
    return lineOrientation === "vertical" ? MIN_LINE_THICKNESS : MIN_LINE_LENGTH;
  }
  if (kind === "image") {
    return MIN_MEDIA_WIDTH;
  }
  return MIN_ELEMENT_WIDTH;
}

function getMinimumHeightForElement(
  kind: ElementKind,
  fontSize: number,
  lineOrientation?: LineOrientation,
) {
  if (kind === "line") {
    return lineOrientation === "vertical" ? MIN_LINE_LENGTH : MIN_LINE_THICKNESS;
  }
  if (kind === "image") {
    return MIN_MEDIA_HEIGHT;
  }
  return getDefaultElementHeight(kind, fontSize, lineOrientation);
}

function getElementHeight(
  element: Pick<TemplateElement, "fontSize" | "height" | "kind" | "lineOrientation">,
) {
  return Math.max(
    1,
    Math.round(
      element.height ?? getDefaultElementHeight(element.kind, element.fontSize, element.lineOrientation),
    ),
  );
}

function fitContainedSize(
  containerWidth: number,
  containerHeight: number,
  assetWidth: number,
  assetHeight: number,
) {
  if (assetWidth <= 0 || assetHeight <= 0) {
    return {
      height: containerHeight,
      offsetX: 0,
      offsetY: 0,
      width: containerWidth,
    };
  }

  const scale = Math.min(containerWidth / assetWidth, containerHeight / assetHeight);
  const width = assetWidth * scale;
  const height = assetHeight * scale;

  return {
    height,
    offsetX: (containerWidth - width) / 2,
    offsetY: (containerHeight - height) / 2,
    width,
  };
}

function decodeBase64DataUrl(dataUrl: string) {
  const match = dataUrl.match(/^data:([^;]+);base64,(.+)$/);
  if (!match) {
    return null;
  }

  const [, mimeType, base64Payload] = match;
  const binary = atob(base64Payload);
  const bytes = new Uint8Array(binary.length);

  for (let index = 0; index < binary.length; index += 1) {
    bytes[index] = binary.charCodeAt(index);
  }

  return {
    bytes,
    mimeType,
  };
}

function bytesToHex(bytes: Uint8Array) {
  let hex = "";
  for (const value of bytes) {
    hex += value.toString(16).padStart(2, "0").toUpperCase();
  }
  return hex;
}

function createPdfImageResources(elements: TemplateElement[]) {
  const imageElements = elements.filter(
    (element) => element.kind === "image" && typeof element.imageSrc === "string" && element.imageSrc,
  );

  return imageElements.reduce<PdfImageResource[]>((resources, element, index) => {
    const decoded = decodeBase64DataUrl(element.imageSrc ?? "");
    if (!decoded || !/image\/jpe?g/i.test(decoded.mimeType)) {
      return resources;
    }

    const hexPayload = `${bytesToHex(decoded.bytes)}>`;
    resources.push({
      body:
        `<< /Type /XObject /Subtype /Image /Width ${Math.max(1, Math.round(element.imageNaturalWidth ?? element.width))}` +
        ` /Height ${Math.max(1, Math.round(element.imageNaturalHeight ?? getElementHeight(element)))}` +
        " /ColorSpace /DeviceRGB /BitsPerComponent 8" +
        " /Filter [/ASCIIHexDecode /DCTDecode]" +
        ` /Length ${hexPayload.length} >>
stream
${hexPayload}
endstream`,
      elementId: element.id,
      name: `/Im${index + 1}`,
      pixelHeight: Math.max(1, Math.round(element.imageNaturalHeight ?? getElementHeight(element))),
      pixelWidth: Math.max(1, Math.round(element.imageNaturalWidth ?? element.width)),
    });

    return resources;
  }, []);
}

function createPdfContentStream(
  elements: TemplateElement[],
  imageResources: Map<string, PdfImageResource>,
) {
  const commands = ["0 g"];

  for (const element of elements) {
    if (element.kind === "line") {
      const elementHeight = getElementHeight(element);
      const pdfX = element.x * PDF_SCALE;
      const pdfY = PDF_PAGE_HEIGHT - (element.y + elementHeight) * PDF_SCALE;
      const pdfWidth = element.width * PDF_SCALE;
      const pdfHeight = elementHeight * PDF_SCALE;

      commands.push(
        `${formatPdfNumber(pdfX)} ${formatPdfNumber(pdfY)} ${formatPdfNumber(pdfWidth)} ${formatPdfNumber(pdfHeight)} re f`,
      );
      continue;
    }

    if (element.kind === "image") {
      const imageResource = imageResources.get(element.id);
      if (!imageResource) {
        continue;
      }

      const boxHeight = getElementHeight(element);
      const fitted = fitContainedSize(
        element.width,
        boxHeight,
        imageResource.pixelWidth,
        imageResource.pixelHeight,
      );
      const pdfX = (element.x + fitted.offsetX) * PDF_SCALE;
      const pdfY =
        PDF_PAGE_HEIGHT - (element.y + fitted.offsetY + fitted.height) * PDF_SCALE;
      const pdfWidth = fitted.width * PDF_SCALE;
      const pdfHeight = fitted.height * PDF_SCALE;

      commands.push("q");
      commands.push(
        `${formatPdfNumber(pdfWidth)} 0 0 ${formatPdfNumber(pdfHeight)} ${formatPdfNumber(pdfX)} ${formatPdfNumber(pdfY)} cm`,
      );
      commands.push(`${imageResource.name} Do`);
      commands.push("Q");
      continue;
    }

    const fontName = element.fontWeight >= 600 ? "/F2" : "/F1";
    const fontSize = element.fontSize * PDF_SCALE;
    const lineHeight = element.fontSize * PDF_LINE_HEIGHT;
    const textStartX = element.x + PDF_TEXT_PADDING_X;
    const textWidth = Math.max(24, element.width - PDF_TEXT_PADDING_X * 2);
    const wrappedLines = wrapTextLines(
      element.content || "",
      textWidth,
      element.fontSize,
      element.fontWeight,
    );

    wrappedLines.forEach((line, index) => {
      if (!line) {
        return;
      }

      const renderedLineWidth = measureTextWidth(line, element.fontSize, element.fontWeight);
      let textX = textStartX;
      if (element.textAlign === "center") {
        textX += Math.max(0, (textWidth - renderedLineWidth) / 2);
      } else if (element.textAlign === "right") {
        textX += Math.max(0, textWidth - renderedLineWidth);
      }

      const baselineY = element.y + PDF_TEXT_PADDING_Y + element.fontSize + index * lineHeight;
      const pdfX = textX * PDF_SCALE;
      const pdfY = PDF_PAGE_HEIGHT - baselineY * PDF_SCALE;

      commands.push("BT");
      commands.push(`${fontName} ${formatPdfNumber(fontSize)} Tf`);
      commands.push(`1 0 0 1 ${formatPdfNumber(pdfX)} ${formatPdfNumber(pdfY)} Tm`);
      commands.push(`<${encodePdfText(line)}> Tj`);
      commands.push("ET");
    });
  }

  return commands.join("\n");
}

function createPdfBlob(elements: TemplateElement[]) {
  const imageResources = createPdfImageResources(elements);
  const imageResourceMap = new Map(imageResources.map((resource) => [resource.elementId, resource]));
  const contentStream = createPdfContentStream(elements, imageResourceMap);
  const imageObjectNumberStart = 6;
  const contentObjectNumber = imageObjectNumberStart + imageResources.length;
  const xObjectResources = imageResources.length
    ? ` /XObject << ${imageResources
        .map((resource, index) => `${resource.name} ${imageObjectNumberStart + index} 0 R`)
        .join(" ")} >>`
    : "";
  const objects = [
    "<< /Type /Catalog /Pages 2 0 R >>",
    "<< /Type /Pages /Count 1 /Kids [3 0 R] >>",
    `<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ${formatPdfNumber(PDF_PAGE_WIDTH)} ${formatPdfNumber(PDF_PAGE_HEIGHT)}] /Resources << /Font << /F1 4 0 R /F2 5 0 R >>${xObjectResources} >> /Contents ${contentObjectNumber} 0 R >>`,
    "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>",
    "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>",
    ...imageResources.map((resource) => resource.body),
    `<< /Length ${contentStream.length} >>\nstream\n${contentStream}\nendstream`,
  ];

  let pdf = "%PDF-1.4\n";
  const offsets = [0];

  objects.forEach((body, index) => {
    offsets.push(pdf.length);
    pdf += `${index + 1} 0 obj\n${body}\nendobj\n`;
  });

  const xrefOffset = pdf.length;
  pdf += `xref\n0 ${objects.length + 1}\n`;
  pdf += "0000000000 65535 f \n";

  for (let index = 1; index <= objects.length; index += 1) {
    pdf += `${String(offsets[index]).padStart(10, "0")} 00000 n \n`;
  }

  pdf += `trailer\n<< /Size ${objects.length + 1} /Root 1 0 R >>\nstartxref\n${xrefOffset}\n%%EOF`;

  return new Blob([pdf], { type: "application/pdf" });
}

function generateId() {
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
    return crypto.randomUUID();
  }
  return `invoice-template-${Date.now()}-${Math.random().toString(16).slice(2)}`;
}

function normalizeFontWeight(value: unknown): FontWeight {
  return value === 500 || value === 600 || value === 700 ? value : 400;
}

function normalizeTextAlign(value: unknown): TextAlign {
  return value === "center" || value === "right" ? value : "left";
}

function normalizeElementKind(value: unknown): ElementKind {
  return value === "line" || value === "image" ? value : "text";
}

function normalizeLineOrientation(value: unknown): LineOrientation {
  return value === "vertical" ? "vertical" : "horizontal";
}

function normalizeElement(element: TemplateElement): TemplateElement {
  const kind = normalizeElementKind(element.kind);
  const lineOrientation = kind === "line" ? normalizeLineOrientation(element.lineOrientation) : undefined;
  const fontSize = clamp(Math.round(element.fontSize), MIN_FONT_SIZE, MAX_FONT_SIZE);
  let width = Math.round(element.width);
  let height = getElementHeight({
    fontSize,
    height: element.height,
    kind,
    lineOrientation,
  });

  if (kind === "line") {
    width = clamp(width, getMinimumWidthForElement(kind, lineOrientation), CANVAS_WIDTH);
    height = clamp(height, getMinimumHeightForElement(kind, fontSize, lineOrientation), CANVAS_HEIGHT);
  } else if (kind === "image") {
    width = clamp(width, getMinimumWidthForElement(kind), CANVAS_WIDTH - 32);
    height = clamp(height, getMinimumHeightForElement(kind, fontSize), CANVAS_HEIGHT - 32);
  } else {
    width = clamp(width, getMinimumWidthForElement(kind), CANVAS_WIDTH - 32);
    height = getDefaultElementHeight(kind, fontSize, lineOrientation);
  }

  const x = clamp(snapToGrid(element.x), 0, CANVAS_WIDTH - width);
  const y = clamp(
    snapToGrid(element.y),
    0,
    kind === "text" ? CANVAS_HEIGHT - 40 : CANVAS_HEIGHT - height,
  );
  const imageNaturalWidth =
    kind === "image" && typeof element.imageNaturalWidth === "number" && element.imageNaturalWidth > 0
      ? Math.round(element.imageNaturalWidth)
      : null;
  const imageNaturalHeight =
    kind === "image" && typeof element.imageNaturalHeight === "number" && element.imageNaturalHeight > 0
      ? Math.round(element.imageNaturalHeight)
      : null;

  return {
    ...element,
    kind,
    width,
    height: kind === "text" ? undefined : height,
    fontSize,
    x,
    y,
    fontWeight: normalizeFontWeight(element.fontWeight),
    textAlign: normalizeTextAlign(element.textAlign),
    lineOrientation,
    imageSrc: kind === "image" ? element.imageSrc ?? null : undefined,
    imageNaturalWidth,
    imageNaturalHeight,
  };
}

function findPaletteItem(paletteItems: PaletteItem[], paletteId: string) {
  return paletteItems.find((entry) => entry.id === paletteId) ?? null;
}

function requirePaletteItem(paletteItems: PaletteItem[], paletteId: string) {
  const paletteItem = findPaletteItem(paletteItems, paletteId);
  if (!paletteItem) {
    throw new Error(`Palette item ${paletteId} is not configured.`);
  }
  return paletteItem;
}

function getPalettePreviewText(paletteItem: PaletteItem, user: UserOption | null) {
  if (paletteItem.kind === "image") {
    return "Bild auswaehlen";
  }
  if (paletteItem.kind === "line") {
    return "Linie";
  }
  return paletteItem.defaultContent(user);
}

function isElementContentEditable(
  element: Pick<TemplateElement, "kind" | "paletteId">,
  lockedTextPaletteIds: ReadonlySet<string>,
) {
  return element.kind === "text" && !lockedTextPaletteIds.has(element.paletteId);
}

function createElementFromPalette(
  paletteItem: PaletteItem,
  user: UserOption | null,
  position: { x: number; y: number },
): TemplateElement {
  return normalizeElement({
    id: generateId(),
    paletteId: paletteItem.id,
    kind: paletteItem.kind ?? "text",
    label: paletteItem.label,
    content: paletteItem.defaultContent(user),
    x: position.x,
    y: position.y,
    width: paletteItem.width,
    height: paletteItem.height,
    fontSize: paletteItem.fontSize,
    fontWeight: paletteItem.fontWeight,
    textAlign: paletteItem.textAlign,
    lineOrientation: paletteItem.lineOrientation,
    imageSrc: null,
    imageNaturalWidth: null,
    imageNaturalHeight: null,
  });
}

function createInvoiceStarterLayout(user: UserOption | null, paletteItems: PaletteItem[]) {
  const horizontalLine = (x: number, y: number, width: number) =>
    normalizeElement({
      ...createElementFromPalette(requirePaletteItem(paletteItems, "line"), user, { x, y }),
      width,
    });

  return [
    createElementFromPalette(requirePaletteItem(paletteItems, "issuer-name"), user, { x: 56, y: 56 }),
    createElementFromPalette(requirePaletteItem(paletteItems, "issuer-street"), user, { x: 56, y: 86 }),
    createElementFromPalette(requirePaletteItem(paletteItems, "issuer-house-number"), user, { x: 260, y: 86 }),
    createElementFromPalette(requirePaletteItem(paletteItems, "issuer-postal-code"), user, { x: 56, y: 110 }),
    createElementFromPalette(requirePaletteItem(paletteItems, "issuer-city"), user, { x: 140, y: 110 }),
    createElementFromPalette(requirePaletteItem(paletteItems, "issuer-contact"), user, { x: 56, y: 130 }),
    createElementFromPalette(requirePaletteItem(paletteItems, "invoice-title"), user, { x: 480, y: 56 }),
    createElementFromPalette(requirePaletteItem(paletteItems, "invoice-number"), user, { x: 480, y: 96 }),
    createElementFromPalette(requirePaletteItem(paletteItems, "invoice-date"), user, { x: 480, y: 122 }),
    createElementFromPalette(requirePaletteItem(paletteItems, "invoice-due-date"), user, { x: 480, y: 148 }),
    horizontalLine(56, 200, 646),
    createElementFromPalette(requirePaletteItem(paletteItems, "customer-name"), user, { x: 56, y: 220 }),
    createElementFromPalette(requirePaletteItem(paletteItems, "customer-street"), user, { x: 56, y: 246 }),
    createElementFromPalette(requirePaletteItem(paletteItems, "customer-house-number"), user, { x: 260, y: 246 }),
    createElementFromPalette(requirePaletteItem(paletteItems, "customer-postal-code"), user, { x: 56, y: 270 }),
    createElementFromPalette(requirePaletteItem(paletteItems, "customer-city"), user, { x: 140, y: 270 }),
    createElementFromPalette(requirePaletteItem(paletteItems, "customer-number"), user, { x: 56, y: 290 }),
    horizontalLine(56, 330, 646),
    horizontalLine(56, 560, 646),
    createElementFromPalette(requirePaletteItem(paletteItems, "payment-terms"), user, { x: 56, y: 360 }),
    createElementFromPalette(requirePaletteItem(paletteItems, "bank-details"), user, { x: 56, y: 400 }),
  ];
}

function readFileAsDataUrl(file: File) {
  return new Promise<string>((resolve, reject) => {
    const reader = new FileReader();
    reader.onerror = () => reject(new Error("Bild konnte nicht geladen werden."));
    reader.onload = () => {
      if (typeof reader.result === "string") {
        resolve(reader.result);
        return;
      }
      reject(new Error("Bild konnte nicht geladen werden."));
    };
    reader.readAsDataURL(file);
  });
}

function loadImageAsset(dataUrl: string) {
  return new Promise<UploadedImageAsset>((resolve, reject) => {
    const image = new Image();
    image.onerror = () => reject(new Error("Bild konnte nicht gelesen werden."));
    image.onload = () =>
      resolve({
        dataUrl,
        height: image.naturalHeight,
        width: image.naturalWidth,
      });
    image.src = dataUrl;
  });
}

async function convertImageFileToJpeg(file: File) {
  const source = await readFileAsDataUrl(file);
  const asset = await loadImageAsset(source);
  const canvas = document.createElement("canvas");
  canvas.width = asset.width;
  canvas.height = asset.height;

  const context = canvas.getContext("2d");
  if (!context) {
    throw new Error("Bild konnte nicht verarbeitet werden.");
  }

  const image = await loadImageAsset(source);
  context.fillStyle = "#ffffff";
  context.fillRect(0, 0, canvas.width, canvas.height);
  const renderImage = new Image();

  await new Promise<void>((resolve, reject) => {
    renderImage.onerror = () => reject(new Error("Bild konnte nicht verarbeitet werden."));
    renderImage.onload = () => {
      context.drawImage(renderImage, 0, 0, canvas.width, canvas.height);
      resolve();
    };
    renderImage.src = image.dataUrl;
  });

  return {
    dataUrl: canvas.toDataURL("image/jpeg", 0.92),
    height: asset.height,
    width: asset.width,
  };
}

function normalizeTemplateElements(raw: unknown) {
  if (!Array.isArray(raw)) {
    return null;
  }

  const elements = raw
    .map((entry) => {
      if (!entry || typeof entry !== "object") {
        return null;
      }

      const candidate = entry as Partial<TemplateElement>;
      if (
        typeof candidate.id !== "string" ||
        typeof candidate.paletteId !== "string" ||
        typeof candidate.label !== "string" ||
        typeof candidate.content !== "string" ||
        typeof candidate.x !== "number" ||
        typeof candidate.y !== "number" ||
        typeof candidate.width !== "number" ||
        typeof candidate.fontSize !== "number"
      ) {
        return null;
      }

      return normalizeElement({
        id: candidate.id,
        paletteId: candidate.paletteId,
        kind: normalizeElementKind(candidate.kind),
        label: candidate.label,
        content: candidate.content,
        x: candidate.x,
        y: candidate.y,
        width: candidate.width,
        height: typeof candidate.height === "number" ? candidate.height : undefined,
        fontSize: candidate.fontSize,
        fontWeight: normalizeFontWeight(candidate.fontWeight),
        textAlign: normalizeTextAlign(candidate.textAlign),
        lineOrientation: normalizeLineOrientation(candidate.lineOrientation),
        imageSrc: typeof candidate.imageSrc === "string" ? candidate.imageSrc : null,
        imageNaturalWidth:
          typeof candidate.imageNaturalWidth === "number" ? candidate.imageNaturalWidth : null,
        imageNaturalHeight:
          typeof candidate.imageNaturalHeight === "number" ? candidate.imageNaturalHeight : null,
      });
    })
    .filter((entry): entry is TemplateElement => entry !== null);

  return elements;
}

function formatTemplateTimestamp(value: string | null) {
  if (!value) {
    return null;
  }

  const parsed = new Date(value);
  if (Number.isNaN(parsed.getTime())) {
    return null;
  }

  return new Intl.DateTimeFormat("de-DE", {
    dateStyle: "medium",
    timeStyle: "short",
  }).format(parsed);
}

function readDragPayload(event: DragEvent<HTMLElement>): DragPayload | null {
  const raw = event.dataTransfer.getData(DRAG_DATA_TYPE);
  if (!raw) {
    return null;
  }

  try {
    const parsed = JSON.parse(raw) as DragPayload;
    if (parsed.kind === "palette" && typeof parsed.paletteId === "string") {
      return parsed;
    }
    if (parsed.kind === "element" && typeof parsed.elementId === "string") {
      return parsed;
    }
    return null;
  } catch {
    return null;
  }
}

export default function InvoiceTemplatePage() {
  const { user } = useSession();
  const pageRef = useRef<HTMLDivElement | null>(null);
  const cardRef = useRef<HTMLElement | null>(null);
  const canvasRef = useRef<HTMLDivElement | null>(null);
  const canvasShellRef = useRef<HTMLDivElement | null>(null);
  const dragDepthRef = useRef(0);
  const mouseDragCleanupRef = useRef<(() => void) | null>(null);
  const moveOffsetRef = useRef<{ x: number; y: number } | null>(null);

  const [elements, setElements] = useState<TemplateElement[]>(() =>
    createInvoiceStarterLayout(user, INVOICE_PALETTE_ITEMS),
  );
  const [selectedElementId, setSelectedElementId] = useState<string | null>(null);
  const [isCanvasActive, setIsCanvasActive] = useState(false);
  const [draggingElementId, setDraggingElementId] = useState<string | null>(null);
  const [resizingElementId, setResizingElementId] = useState<string | null>(null);
  const [pdfError, setPdfError] = useState<string | null>(null);
  const [templateError, setTemplateError] = useState<string | null>(null);
  const [templateUpdatedAt, setTemplateUpdatedAt] = useState<string | null>(null);
  const [isTemplateLoading, setIsTemplateLoading] = useState(false);
  const [isTemplateSaving, setIsTemplateSaving] = useState(false);
  const [isTemplateApiAvailable, setIsTemplateApiAvailable] = useState(true);
  const [pdfPreviewUrl, setPdfPreviewUrl] = useState<string | null>(null);
  const [pageViewportHeight, setPageViewportHeight] = useState<number | null>(null);
  const [canvasViewport, setCanvasViewport] = useState<CanvasViewport>({
    height: CANVAS_HEIGHT,
    scale: 1,
    syncPanelHeight: false,
    width: CANVAS_WIDTH,
  });

  const selectedElement = elements.find((entry) => entry.id === selectedElementId) ?? null;
  const selectedElementHeight = selectedElement ? getElementHeight(selectedElement) : null;
  const selectedElementContentEditable = selectedElement
    ? isElementContentEditable(selectedElement, INVOICE_LOCKED_TEXT_PALETTE_IDS)
    : false;
  const selectedElementMinimumWidth = selectedElement
    ? getMinimumWidthForElement(selectedElement.kind, selectedElement.lineOrientation)
    : MIN_ELEMENT_WIDTH;
  const selectedElementMinimumHeight = selectedElement
    ? getMinimumHeightForElement(
        selectedElement.kind,
        selectedElement.fontSize,
        selectedElement.lineOrientation,
      )
    : MIN_MEDIA_HEIGHT;
  const templateTimestampLabel = formatTemplateTimestamp(templateUpdatedAt);
  const emptyCanvasMessage = "Ziehen Sie links ein Element auf diese Fläche, um Ihre Rechnungsvorlage zu starten.";

  useEffect(() => {
    if (!user) {
      const starterLayout = createInvoiceStarterLayout(null, INVOICE_PALETTE_ITEMS);
      setElements(starterLayout);
      setSelectedElementId(starterLayout[0]?.id ?? null);
      setResizingElementId(null);
      setIsTemplateApiAvailable(true);
      setIsTemplateLoading(false);
      setTemplateUpdatedAt(null);
      setTemplateError(null);
      return;
    }

    let cancelled = false;
    setTemplateError(null);
    setIsTemplateLoading(true);

    void apiGet<InvoiceTemplateResponse>("/session/invoice-template")
      .then((response) => {
        if (cancelled) {
          return;
        }

        setIsTemplateApiAvailable(true);
        setTemplateUpdatedAt(response.updatedAt);
        if (!response.stored) {
          const starterLayout = createInvoiceStarterLayout(user, INVOICE_PALETTE_ITEMS);
          setElements(starterLayout);
          setSelectedElementId(starterLayout[0]?.id ?? null);
          setResizingElementId(null);
          return;
        }

        const loadedElements = normalizeTemplateElements(response.elements);
        if (!loadedElements) {
          throw new Error("Gespeichertes Template konnte nicht geladen werden.");
        }

        setElements(loadedElements);
        setSelectedElementId(loadedElements[0]?.id ?? null);
        setResizingElementId(null);
      })
      .catch((error) => {
        if (!cancelled) {
          if (error instanceof ApiError && error.status === 404) {
            const starterLayout = createInvoiceStarterLayout(user, INVOICE_PALETTE_ITEMS);
            setElements(starterLayout);
            setSelectedElementId(starterLayout[0]?.id ?? null);
            setResizingElementId(null);
            setTemplateUpdatedAt(null);
            setTemplateError(null);
            return;
          }
          setTemplateError((error as Error).message);
        }
      })
      .finally(() => {
        if (!cancelled) {
          setIsTemplateLoading(false);
        }
      });

    return () => {
      cancelled = true;
    };
  }, [user]);

  useEffect(() => {
    if (selectedElementId && elements.some((entry) => entry.id === selectedElementId)) {
      return;
    }
    setSelectedElementId(elements[0]?.id ?? null);
  }, [elements, selectedElementId]);

  useEffect(() => {
    return () => {
      mouseDragCleanupRef.current?.();
    };
  }, []);

  useEffect(() => {
    return () => {
      if (pdfPreviewUrl) {
        URL.revokeObjectURL(pdfPreviewUrl);
      }
    };
  }, [pdfPreviewUrl]);

  useEffect(() => {
    if (!pdfPreviewUrl) {
      return;
    }

    function handleEscape(event: KeyboardEvent) {
      if (event.key === "Escape") {
        setPdfPreviewUrl(null);
      }
    }

    window.addEventListener("keydown", handleEscape);
    return () => {
      window.removeEventListener("keydown", handleEscape);
    };
  }, [pdfPreviewUrl]);

  useEffect(() => {
    let frameId = 0;

    function updateCanvasViewport() {
      cancelAnimationFrame(frameId);
      frameId = window.requestAnimationFrame(() => {
        const pageElement = pageRef.current;
        const cardElement = cardRef.current;
        const canvasShellElement = canvasShellRef.current;
        if (!pageElement || !cardElement || !canvasShellElement) {
          return;
        }

        const pageRect = pageElement.getBoundingClientRect();
        const nextPageHeight = Math.max(window.innerHeight - pageRect.top - 36, 420);
        setPageViewportHeight((current) => (current === nextPageHeight ? current : nextPageHeight));

        const cardStyles = window.getComputedStyle(cardElement);
        const shellStyles = window.getComputedStyle(canvasShellElement);
        const syncPanelHeight = window.innerWidth > 1200;
        const cardPaddingBottom = Number.parseFloat(cardStyles.paddingBottom) || 0;
        const horizontalPadding =
          (Number.parseFloat(shellStyles.paddingLeft) || 0) +
          (Number.parseFloat(shellStyles.paddingRight) || 0);
        const verticalPadding =
          (Number.parseFloat(shellStyles.paddingTop) || 0) +
          (Number.parseFloat(shellStyles.paddingBottom) || 0);
        const cardRect = cardElement.getBoundingClientRect();
        const shellRect = canvasShellElement.getBoundingClientRect();
        const availableWidth = Math.max(
          Math.floor(canvasShellElement.clientWidth - horizontalPadding),
          320,
        );
        const availableHeight = Math.max(
          Math.floor(cardRect.bottom - shellRect.top - cardPaddingBottom - verticalPadding - CANVAS_BOTTOM_GAP),
          240,
        );
        const nextWidth = Math.min(
          CANVAS_WIDTH,
          availableWidth,
          Math.floor(availableHeight * CANVAS_ASPECT_RATIO),
        );
        const nextHeight = Math.floor(nextWidth / CANVAS_ASPECT_RATIO);
        const nextScale = nextWidth / CANVAS_WIDTH;

        setCanvasViewport((current) => {
          if (
            current.height === nextHeight &&
            current.scale === nextScale &&
            current.syncPanelHeight === syncPanelHeight &&
            current.width === nextWidth
          ) {
            return current;
          }

          return {
            height: nextHeight,
            scale: nextScale,
            syncPanelHeight,
            width: nextWidth,
          };
        });
      });
    }

    const resizeObserver =
      typeof ResizeObserver === "undefined"
        ? null
        : new ResizeObserver(() => {
            updateCanvasViewport();
          });

    if (resizeObserver) {
      if (pageRef.current) {
        resizeObserver.observe(pageRef.current);
      }
      if (cardRef.current) {
        resizeObserver.observe(cardRef.current);
      }
      if (canvasShellRef.current) {
        resizeObserver.observe(canvasShellRef.current);
      }
    }

    updateCanvasViewport();
    window.addEventListener("resize", updateCanvasViewport);
    window.addEventListener("scroll", updateCanvasViewport, { passive: true });

    return () => {
      cancelAnimationFrame(frameId);
      resizeObserver?.disconnect();
      window.removeEventListener("resize", updateCanvasViewport);
      window.removeEventListener("scroll", updateCanvasViewport);
    };
  }, [pdfPreviewUrl]);

  function resetCanvasDragState() {
    dragDepthRef.current = 0;
    setIsCanvasActive(false);
  }

  function updateElement(elementId: string, patch: Partial<TemplateElement>) {
    setElements((current) =>
      current.map((entry) =>
        entry.id === elementId
          ? normalizeElement({
              ...entry,
              ...(!isElementContentEditable(entry, INVOICE_LOCKED_TEXT_PALETTE_IDS) &&
              Object.prototype.hasOwnProperty.call(patch, "content")
                ? { ...patch, content: entry.content }
                : patch),
            })
          : entry,
      ),
    );
  }

  function removeElement(elementId: string) {
    mouseDragCleanupRef.current?.();
    setElements((current) => current.filter((entry) => entry.id !== elementId));
    setSelectedElementId((current) => (current === elementId ? null : current));
    setResizingElementId((current) => (current === elementId ? null : current));
  }

  function placePaletteElement(paletteId: string, clientX: number, clientY: number) {
    const paletteItem = findPaletteItem(INVOICE_PALETTE_ITEMS, paletteId);
    const canvasElement = canvasRef.current;
    if (!paletteItem || !canvasElement) {
      return;
    }

    const rect = canvasElement.getBoundingClientRect();
    const previewHeight = getDefaultElementHeight(
      paletteItem.kind ?? "text",
      paletteItem.fontSize,
      paletteItem.lineOrientation,
    );
    const x = (clientX - rect.left) / canvasViewport.scale - paletteItem.width / 2;
    const y = (clientY - rect.top) / canvasViewport.scale - previewHeight / 2;
    const nextElement = createElementFromPalette(paletteItem, user, { x, y });

    setElements((current) => [...current, nextElement]);
    setSelectedElementId(nextElement.id);
  }

  function moveCanvasElement(elementId: string, clientX: number, clientY: number) {
    const canvasElement = canvasRef.current;
    if (!canvasElement) {
      return;
    }

    const rect = canvasElement.getBoundingClientRect();
    const moveOffset = moveOffsetRef.current;
    const x = (clientX - rect.left) / canvasViewport.scale - (moveOffset?.x ?? 0);
    const y = (clientY - rect.top) / canvasViewport.scale - (moveOffset?.y ?? 0);

    updateElement(elementId, { x, y });
  }

  function handleCanvasDragEnter(event: DragEvent<HTMLDivElement>) {
    event.preventDefault();
    dragDepthRef.current += 1;
    setIsCanvasActive(true);
  }

  function handleCanvasDragOver(event: DragEvent<HTMLDivElement>) {
    event.preventDefault();
    const payload = readDragPayload(event);
    event.dataTransfer.dropEffect = payload?.kind === "element" ? "move" : "copy";
    setIsCanvasActive(true);
  }

  function handleCanvasDragLeave(event: DragEvent<HTMLDivElement>) {
    event.preventDefault();
    dragDepthRef.current = Math.max(0, dragDepthRef.current - 1);
    if (dragDepthRef.current === 0) {
      setIsCanvasActive(false);
    }
  }

  function handleCanvasDrop(event: DragEvent<HTMLDivElement>) {
    event.preventDefault();
    const payload = readDragPayload(event);
    resetCanvasDragState();
    if (!payload) {
      return;
    }

    if (payload.kind === "palette") {
      placePaletteElement(payload.paletteId, event.clientX, event.clientY);
      return;
    }

    moveCanvasElement(payload.elementId, event.clientX, event.clientY);
  }

  function handleResetToStarter() {
    mouseDragCleanupRef.current?.();
    const starterLayout = createInvoiceStarterLayout(user, INVOICE_PALETTE_ITEMS);
    setElements(starterLayout);
    setSelectedElementId(starterLayout[0]?.id ?? null);
    setResizingElementId(null);
  }

  function handleClearCanvas() {
    if (!window.confirm("Moechten Sie wirklich alle Elemente vom Canvas entfernen?")) {
      return;
    }

    mouseDragCleanupRef.current?.();
    setElements([]);
    setSelectedElementId(null);
    setResizingElementId(null);
  }

  function handleCreatePdfPreview() {
    try {
      const pdfBlob = createPdfBlob(elements);
      const nextPreviewUrl = URL.createObjectURL(pdfBlob);
      setPdfPreviewUrl(nextPreviewUrl);
      setPdfError(null);
    } catch {
      setPdfError("PDF konnte nicht erstellt werden.");
    }
  }

  async function handleSaveTemplate() {
    if (!user) {
      setTemplateError("Vorlagen koennen nur fuer angemeldete Benutzer gespeichert werden.");
      return;
    }
    if (!isTemplateApiAvailable) {
      setTemplateError("Template-Speicherung ist auf diesem Server nicht verfuegbar.");
      return;
    }

    setTemplateError(null);
    setIsTemplateSaving(true);

    try {
      const response = await apiPut<InvoiceTemplateResponse>("/session/invoice-template", {
        elements,
      });
      const savedElements = normalizeTemplateElements(response.elements);
      if (!savedElements) {
        throw new Error("Gespeichertes Template konnte nicht gelesen werden.");
      }

      setElements(savedElements);
      setSelectedElementId((current) =>
        savedElements.some((entry) => entry.id === current) ? current : savedElements[0]?.id ?? null,
      );
      setResizingElementId(null);
      setTemplateUpdatedAt(response.updatedAt);
      setIsTemplateApiAvailable(true);
    } catch (error) {
      if (error instanceof ApiError && error.status === 404) {
        setIsTemplateApiAvailable(false);
        setTemplateError("Template-Speicherung ist auf diesem Server nicht verfuegbar.");
      } else {
        setTemplateError((error as Error).message);
      }
    } finally {
      setIsTemplateSaving(false);
    }
  }

  async function handleImageUpload(
    event: ReactChangeEvent<HTMLInputElement>,
    elementId: string,
  ) {
    const file = event.target.files?.[0];
    event.target.value = "";
    if (!file) {
      return;
    }

    const currentElement = elements.find((entry) => entry.id === elementId);
    if (!currentElement || currentElement.kind !== "image") {
      return;
    }

    try {
      const image = await convertImageFileToJpeg(file);
      const nextHeight = Math.max(
        MIN_MEDIA_HEIGHT,
        Math.round((currentElement.width * image.height) / image.width),
      );

      updateElement(elementId, {
        height: nextHeight,
        imageNaturalHeight: image.height,
        imageNaturalWidth: image.width,
        imageSrc: image.dataUrl,
      });
      setPdfError(null);
    } catch (error) {
      setPdfError((error as Error).message);
    }
  }

  function handleElementMouseDown(
    event: ReactMouseEvent<HTMLButtonElement>,
    elementId: string,
  ) {
    const canvasElement = canvasRef.current;
    if (!canvasElement || event.button !== 0) {
      return;
    }

    mouseDragCleanupRef.current?.();

    const rect = event.currentTarget.getBoundingClientRect();
    const startOffset = {
      x: (event.clientX - rect.left) / canvasViewport.scale,
      y: (event.clientY - rect.top) / canvasViewport.scale,
    };

    moveOffsetRef.current = startOffset;
    setSelectedElementId(elementId);
    setDraggingElementId(elementId);
    setResizingElementId(null);
    event.currentTarget.focus();
    event.preventDefault();

    function handleMouseMove(moveEvent: MouseEvent) {
      moveCanvasElement(elementId, moveEvent.clientX, moveEvent.clientY);
    }

    function stopDragging() {
      moveOffsetRef.current = null;
      setDraggingElementId(null);
      window.removeEventListener("mousemove", handleMouseMove);
      window.removeEventListener("mouseup", handleMouseUp);
      mouseDragCleanupRef.current = null;
    }

    function handleMouseUp() {
      stopDragging();
    }

    window.addEventListener("mousemove", handleMouseMove);
    window.addEventListener("mouseup", handleMouseUp);
    mouseDragCleanupRef.current = stopDragging;
  }

  function handleResizeMouseDown(
    event: ReactMouseEvent<HTMLSpanElement>,
    elementId: string,
  ) {
    const currentElement = elements.find((entry) => entry.id === elementId);
    if (!currentElement || event.button !== 0) {
      return;
    }

    mouseDragCleanupRef.current?.();
    setSelectedElementId(elementId);
    setDraggingElementId(null);
    setResizingElementId(elementId);
    event.currentTarget.parentElement?.focus();
    event.preventDefault();
    event.stopPropagation();

    const startWidth = currentElement.width;
    const startHeight = getElementHeight(currentElement);
    const elementKind = currentElement.kind;
    const elementFontSize = currentElement.fontSize;
    const elementLineOrientation = currentElement.lineOrientation;
    const imageNaturalWidth = currentElement.imageNaturalWidth;
    const imageNaturalHeight = currentElement.imageNaturalHeight;
    const startClientX = event.clientX;
    const startClientY = event.clientY;

    function handleMouseMove(moveEvent: MouseEvent) {
      const deltaX = (moveEvent.clientX - startClientX) / canvasViewport.scale;
      const deltaY = (moveEvent.clientY - startClientY) / canvasViewport.scale;
      const nextWidth = Math.max(
        getMinimumWidthForElement(elementKind, elementLineOrientation),
        snapToGrid(startWidth + deltaX),
      );
      const nextHeight = Math.max(
        getMinimumHeightForElement(
          elementKind,
          elementFontSize,
          elementLineOrientation,
        ),
        snapToGrid(startHeight + deltaY),
      );

      if (elementKind === "image") {
        const assetWidth = imageNaturalWidth ?? startWidth;
        const assetHeight = imageNaturalHeight ?? startHeight;
        const widthScale = nextWidth / assetWidth;
        const heightScale = nextHeight / assetHeight;

        if (widthScale <= heightScale) {
          updateElement(elementId, {
            height: Math.max(
              getMinimumHeightForElement(elementKind, elementFontSize),
              Math.floor(((nextWidth * assetHeight) / assetWidth) / GRID_SIZE) * GRID_SIZE,
            ),
            width: nextWidth,
          });
          return;
        }

        updateElement(elementId, {
          height: nextHeight,
          width: Math.max(
            getMinimumWidthForElement(elementKind),
            Math.floor(((nextHeight * assetWidth) / assetHeight) / GRID_SIZE) * GRID_SIZE,
          ),
        });
        return;
      }

      updateElement(elementId, {
        width: nextWidth,
        ...(elementKind === "text" ? {} : { height: nextHeight }),
      });
    }

    function stopResizing() {
      setResizingElementId(null);
      window.removeEventListener("mousemove", handleMouseMove);
      window.removeEventListener("mouseup", handleMouseUp);
      mouseDragCleanupRef.current = null;
    }

    function handleMouseUp() {
      stopResizing();
    }

    window.addEventListener("mousemove", handleMouseMove);
    window.addEventListener("mouseup", handleMouseUp);
    mouseDragCleanupRef.current = stopResizing;
  }

  function handleElementKeyDown(
    event: ReactKeyboardEvent<HTMLButtonElement>,
    elementId: string,
  ) {
    const currentElement = elements.find((entry) => entry.id === elementId);
    if (!currentElement) {
      return;
    }

    if (event.key === "Delete") {
      event.preventDefault();
      removeElement(elementId);
      return;
    }

    let patch: Partial<TemplateElement> | null = null;

    if (event.key === "ArrowLeft") {
      patch = { x: currentElement.x - GRID_SIZE };
    } else if (event.key === "ArrowRight") {
      patch = { x: currentElement.x + GRID_SIZE };
    } else if (event.key === "ArrowUp") {
      patch = { y: currentElement.y - GRID_SIZE };
    } else if (event.key === "ArrowDown") {
      patch = { y: currentElement.y + GRID_SIZE };
    }

    if (!patch) {
      return;
    }

    event.preventDefault();
    setSelectedElementId(elementId);
    updateElement(elementId, patch);
  }

  const panelStyle = canvasViewport.syncPanelHeight
    ? { height: `${canvasViewport.height}px` }
    : undefined;
  const pageStyle =
    pageViewportHeight && canvasViewport.syncPanelHeight
      ? { height: `${pageViewportHeight}px` }
      : undefined;

  return (
    <div ref={pageRef} className="page-stack invoice-template-page" style={pageStyle}>
      <section ref={cardRef} className="section-card invoice-template-page__card">
        <div className="section-card__header">
          <div>
            <h3>Rechnungsvorlage bearbeiten</h3>
          </div>

          <div className="page-actions">
            <button type="button" className="accent-button" onClick={handleCreatePdfPreview}>
              PDF erstellen
            </button>
            <button
              type="button"
              className="secondary-button"
              onClick={() => {
                void handleSaveTemplate();
              }}
              disabled={isTemplateLoading || isTemplateSaving || !isTemplateApiAvailable}
            >
              {isTemplateSaving ? "Speichert..." : "Template speichern"}
            </button>
            <button type="button" className="secondary-button" onClick={handleResetToStarter}>
              Startlayout laden
            </button>
            <button type="button" className="ghost-button" onClick={handleClearCanvas}>
              Canvas leeren
            </button>
          </div>
        </div>

        {pdfError ? <div className="alert alert--error">{pdfError}</div> : null}
        {templateError ? <div className="alert alert--error">{templateError}</div> : null}

        <div className="invoice-template">
          <aside className="invoice-template__panel" style={panelStyle}>
            <div className="invoice-template__panel-header">
              <div>
                <h4>Elemente</h4>
              </div>
            </div>

            <div className="invoice-template__palette">
              {INVOICE_PALETTE_GROUPS.map((group) => (
                <section key={group.category} className="invoice-template__palette-group">
                  <div className="invoice-template__palette-group-header">
                    <h5>{group.title}</h5>
                  </div>

                  <div className="invoice-template__palette-grid">
                    {INVOICE_PALETTE_ITEMS.filter((item) => item.category === group.category).map((item) => (
                      <button
                        key={item.id}
                        type="button"
                        draggable
                        className="invoice-template__tile"
                        onDragStart={(event) => {
                          event.dataTransfer.effectAllowed = "copy";
                          event.dataTransfer.setData(
                            DRAG_DATA_TYPE,
                            JSON.stringify(
                              { kind: "palette", paletteId: item.id } satisfies DragPayload,
                            ),
                          );
                        }}
                        onDragEnd={resetCanvasDragState}
                      >
                        <span>{getPalettePreviewText(item, user)}</span>
                      </button>
                    ))}
                  </div>
                </section>
              ))}
            </div>
          </aside>

          <div className="invoice-template__canvas-column">
            <div className="invoice-template__canvas-header">
              <div>
                <h4>Rechnungsvorlage</h4>
                <span className="invoice-template__panel-note">
                  {isTemplateLoading
                    ? "Gespeicherte Vorlage wird geladen..."
                    : !isTemplateApiAvailable
                      ? "Template-API auf diesem Server nicht verfuegbar"
                    : templateTimestampLabel
                      ? `In Datenbank gespeichert: ${templateTimestampLabel}`
                      : "Noch keine gespeicherte Vorlage vorhanden"}
                </span>
              </div>
            </div>

            <div ref={canvasShellRef} className="invoice-template__canvas-shell">
              <div
                className="invoice-template__canvas-stage"
                style={{
                  height: `${canvasViewport.height}px`,
                  width: `${canvasViewport.width}px`,
                }}
              >
                <div
                  ref={canvasRef}
                  className={`invoice-template__canvas ${isCanvasActive ? "is-active" : ""}`}
                  style={{ transform: `scale(${canvasViewport.scale})` }}
                  onDragEnter={handleCanvasDragEnter}
                  onDragOver={handleCanvasDragOver}
                  onDragLeave={handleCanvasDragLeave}
                  onDrop={handleCanvasDrop}
                >
                  {elements.length ? (
                    elements.map((element) => (
                      <button
                        key={element.id}
                        type="button"
                        className={`invoice-template__canvas-element invoice-template__canvas-element--${element.kind} ${selectedElementId === element.id ? "is-selected" : ""} ${draggingElementId === element.id ? "is-dragging" : ""} ${resizingElementId === element.id ? "is-resizing" : ""}`}
                        style={{
                          left: `${element.x}px`,
                          top: `${element.y}px`,
                          width: `${element.width}px`,
                          ...(element.kind === "text"
                            ? {
                                fontSize: `${element.fontSize}px`,
                                fontWeight: element.fontWeight,
                                textAlign: element.textAlign,
                              }
                            : { height: `${getElementHeight(element)}px` }),
                        }}
                        onMouseDown={(event) => handleElementMouseDown(event, element.id)}
                        onKeyDown={(event) => handleElementKeyDown(event, element.id)}
                        onClick={() => setSelectedElementId(element.id)}
                        onFocus={() => setSelectedElementId(element.id)}
                      >
                        {element.kind === "text" ? (
                          <span className="invoice-template__canvas-element-text">
                            {element.content || "Text eingeben"}
                          </span>
                        ) : null}
                        {element.kind === "line" ? (
                          <span
                            aria-hidden="true"
                            className={`invoice-template__canvas-line invoice-template__canvas-line--${element.lineOrientation ?? "horizontal"}`}
                          />
                        ) : null}
                        {element.kind === "image" ? (
                          element.imageSrc ? (
                            <img
                              alt=""
                              className="invoice-template__canvas-image"
                              src={element.imageSrc}
                            />
                          ) : (
                            <span className="invoice-template__canvas-image-placeholder">
                              Bild auswaehlen
                            </span>
                          )
                        ) : null}
                        {selectedElementId === element.id ? (
                          <span
                            aria-hidden="true"
                            className="invoice-template__canvas-element-resize-handle"
                            onMouseDown={(event) => handleResizeMouseDown(event, element.id)}
                          />
                        ) : null}
                      </button>
                    ))
                  ) : (
                    <div className="invoice-template__canvas-empty">{emptyCanvasMessage}</div>
                  )}
                </div>
              </div>
            </div>
          </div>

          <aside className="invoice-template__panel" style={panelStyle}>
            <div className="invoice-template__panel-header">
              <div>
                <h4>Einstellungen</h4>
              </div>
              <span className="invoice-template__panel-note">
                {selectedElement ? "" : "Noch nichts ausgewählt"}
              </span>
            </div>

            {selectedElement ? (
              <div className="invoice-template__inspector">
                <label className="field">
                  <span>Elementname</span>
                  <input
                    value={selectedElement.label}
                    onChange={(event) =>
                      updateElement(selectedElement.id, { label: event.target.value })
                    }
                  />
                </label>

                {selectedElement.kind === "text" ? (
                  <label className="field">
                    <span>Inhalt</span>
                    <textarea
                      value={selectedElement.content}
                      readOnly={!selectedElementContentEditable}
                      onChange={
                        selectedElementContentEditable
                          ? (event) =>
                              updateElement(selectedElement.id, {
                                content: event.target.value,
                              })
                          : undefined
                      }
                    />
                    {!selectedElementContentEditable ? (
                      <small>Dieser Inhalt ist fuer dieses Element gesperrt.</small>
                    ) : null}
                  </label>
                ) : null}

                {selectedElement.kind === "image" ? (
                  <div className="field">
                    <span>Bilddatei</span>
                    <input
                      type="file"
                      accept="image/*"
                      onChange={(event) => {
                        void handleImageUpload(event, selectedElement.id);
                      }}
                    />
                    <small>
                      {selectedElement.imageSrc
                        ? "Bild geladen. Ein neuer Upload ersetzt die aktuelle Datei."
                        : "Noch kein Bild hinterlegt."}
                    </small>
                  </div>
                ) : null}

                <div className="field-grid">
                  <label className="field">
                    <span>X-Position</span>
                    <input
                      type="number"
                      value={selectedElement.x}
                      min={0}
                      max={CANVAS_WIDTH}
                      step={GRID_SIZE}
                      onChange={(event) =>
                        updateElement(selectedElement.id, { x: Number(event.target.value) })
                      }
                    />
                  </label>

                  <label className="field">
                    <span>Y-Position</span>
                    <input
                      type="number"
                      value={selectedElement.y}
                      min={0}
                      max={CANVAS_HEIGHT}
                      step={GRID_SIZE}
                      onChange={(event) =>
                        updateElement(selectedElement.id, { y: Number(event.target.value) })
                      }
                    />
                  </label>

                  <label className="field">
                    <span>Breite</span>
                    <input
                      type="number"
                      value={selectedElement.width}
                      min={selectedElementMinimumWidth}
                      max={CANVAS_WIDTH}
                      onChange={(event) =>
                        updateElement(selectedElement.id, { width: Number(event.target.value) })
                      }
                    />
                  </label>

                  {selectedElement.kind === "text" ? (
                    <label className="field">
                      <span>Schriftgroesse</span>
                      <input
                        type="number"
                        value={selectedElement.fontSize}
                        min={MIN_FONT_SIZE}
                        max={MAX_FONT_SIZE}
                        onChange={(event) =>
                          updateElement(selectedElement.id, { fontSize: Number(event.target.value) })
                        }
                      />
                    </label>
                  ) : (
                    <label className="field">
                      <span>Hoehe</span>
                      <input
                        type="number"
                        value={selectedElementHeight ?? 0}
                        min={selectedElementMinimumHeight}
                        max={CANVAS_HEIGHT}
                        onChange={(event) =>
                          updateElement(selectedElement.id, { height: Number(event.target.value) })
                        }
                      />
                    </label>
                  )}
                </div>

                {selectedElement.kind === "text" ? (
                  <>
                    <label className="field">
                      <span>Schriftschnitt</span>
                      <select
                        value={selectedElement.fontWeight}
                        onChange={(event) =>
                          updateElement(selectedElement.id, {
                            fontWeight: Number(event.target.value) as FontWeight,
                          })
                        }
                      >
                        <option value="400">Normal</option>
                        <option value="500">Mittel</option>
                        <option value="600">Halbfett</option>
                        <option value="700">Fett</option>
                      </select>
                    </label>

                    <div className="field">
                      <span>Ausrichtung</span>
                      <div className="choice-row">
                        {(["left", "center", "right"] as const).map((align) => (
                          <button
                            key={align}
                            type="button"
                            className={`choice-chip ${selectedElement.textAlign === align ? "is-selected" : ""}`}
                            onClick={() => updateElement(selectedElement.id, { textAlign: align })}
                          >
                            {align === "left" ? "Links" : align === "center" ? "Zentriert" : "Rechts"}
                          </button>
                        ))}
                      </div>
                    </div>
                  </>
                ) : null}

                {selectedElement.kind === "line" ? (
                  <label className="field">
                    <span>Ausrichtung</span>
                    <select
                      value={selectedElement.lineOrientation ?? "horizontal"}
                      onChange={(event) =>
                        updateElement(selectedElement.id, {
                          height: selectedElement.width,
                          lineOrientation: event.target.value as LineOrientation,
                          width: selectedElementHeight ?? selectedElement.width,
                        })
                      }
                    >
                      <option value="horizontal">Horizontal</option>
                      <option value="vertical">Vertikal</option>
                    </select>
                  </label>
                ) : null}

                <div className="invoice-template__inspector-actions">
                  {selectedElement.kind === "image" && selectedElement.imageSrc ? (
                    <button
                      type="button"
                      className="secondary-button"
                      onClick={() =>
                        updateElement(selectedElement.id, {
                          imageNaturalHeight: null,
                          imageNaturalWidth: null,
                          imageSrc: null,
                        })
                      }
                    >
                      Bild entfernen
                    </button>
                  ) : null}
                  <button
                    type="button"
                    className="ghost-button"
                    onClick={() => removeElement(selectedElement.id)}
                  >
                    Element entfernen
                  </button>
                </div>
              </div>
            ) : (
              <div className="empty-state">
                Klicken Sie ein Element auf dem Canvas an, damit hier seine Einstellungen erscheinen.
              </div>
            )}
          </aside>
        </div>
      </section>

      {pdfPreviewUrl ? (
        <div className="dialog-backdrop" onClick={() => setPdfPreviewUrl(null)}>
          <div className="dialog dialog--wide" onClick={(event) => event.stopPropagation()}>
            <div className="dialog__header">
              <div>
                <h4>PDF-Vorschau</h4>
              </div>

              <div className="dialog__actions">
                <a
                  href={pdfPreviewUrl}
                  download="rechnung-template.pdf"
                  className="secondary-button"
                >
                  PDF herunterladen
                </a>
                <button
                  type="button"
                  className="ghost-button"
                  onClick={() => setPdfPreviewUrl(null)}
                >
                  Schließen
                </button>
              </div>
            </div>

            <div className="dialog__body dialog__body--pdf">
              <iframe
                className="dialog__frame"
                src={pdfPreviewUrl}
                title="PDF-Vorschau Rechnungsvorlage"
              />
            </div>
          </div>
        </div>
      ) : null}
    </div>
  );
}
