/**
 * Lightweight element registration for client-side PDF generation.
 *
 * This file registers only the `toHTML` serializers needed by HTMLGenerator,
 * without pulling in React editor components, icons, or other heavy deps
 * that the full registry.tsx requires.
 *
 * **Invoice “sticky” export:** To fix an element on every PDF page (ignore product-table
 * reflow), add `sticky?: boolean` to its props in the editor, append `stickyDataAttr()` from
 * `@pdfdraft/core/lib/stickyExportAttr` on the element root in both this file and
 * `registry.tsx`, and add `ElementStickySettings` to the element’s `ElementControls` panel.
 * Export detection lives in `isStickyExportElement` (same module).
 */
import { StyleGenerator } from '@pdfdraft/core/lib/html';
import { generateStyleString } from '@pdfdraft/core/lib/preview';
import { placeholder } from '@pdfdraft/core/lib/placeholder';
import { useElementsStore, type ElementType } from '@pdfdraft/elements';
import { productTableToHTML } from '../elements/product-table/toHTML';
import { PRODUCT_TABLE_NAMESPACE } from '../elements/product-table/constants/types';
import { totalBlockToHTML } from '../elements/order-totals/toHTML';
import { TOTAL_BLOCK_NAMESPACE } from '../elements/order-totals/constants/types';

const DEFAULT_FONT_FAMILY = 'Inter';
const DEFAULT_FONT_WEIGHT = '400';
const DEFAULT_FONT_SIZE = '16px';
const DEFAULT_LINE_HEIGHT = 1.4;
const DEFAULT_LETTER_SPACING = '0px';
const DEFAULT_SHAPE_COLOR = '#000';
const DEFAULT_SHAPE_SIZE = 54;
const STICKY_EXPORT_ATTR = 'data-pdfdraft-sticky';

const COMMON_STYLES = {
	lineHeight: `line-height: ${DEFAULT_LINE_HEIGHT}`,
	letterSpacing: `letter-spacing: ${DEFAULT_LETTER_SPACING}`,
	wordWrap: 'word-wrap: break-word',
	fontVariant:
		'-webkit-font-variant-ligatures: none;font-variant-ligatures: none',
	fullSize: 'height: 100%; width: 100%',
	absolute: 'position: absolute',
	block: 'display: block',
} as const;

const styleGenerator = new StyleGenerator();

// ---------------------------------------------------------------------------
// Font style helper
// ---------------------------------------------------------------------------
interface TextProps {
	sticky?: boolean;
	globalStyle?: {
		fontFamily?: string;
		fontWeight?: string;
		fontSize?: number;
		color?: string;
	};
}

const generateFontStyles = (
	globalStyle: TextProps['globalStyle'] = {},
): string => {
	const {
		fontFamily = DEFAULT_FONT_FAMILY,
		fontWeight = DEFAULT_FONT_WEIGHT,
		fontSize,
		color,
	} = globalStyle;

	return [
		`font-family:${fontFamily}, Inter, sans-serif`,
		`font-weight:${fontWeight}`,
		`font-size:${fontSize ? `${fontSize}px` : DEFAULT_FONT_SIZE}`,
		color ? `color:${color}` : '',
	]
		.filter(Boolean)
		.join(';');
};

function advancedSelectorToHTML(data: ElementData): string {
	const styleObj = data.style ? { ...data.style } : {};
	delete (styleObj as Record<string, unknown>).backgroundColor;
	const style = generateStyleString(styleObj);
	const textAlign = data.style?.textAlign ?? 'start';
	const raw =
		(data?.props?.content as string | undefined) ??
		(data?.props?.mergeTag as string | undefined) ??
		'';
	const escaped = raw
		.replace(/&/g, '&amp;')
		.replace(/</g, '&lt;')
		.replace(/>/g, '&gt;')
		.replace(/\n/g, '<br>');
	const innerStyle = [
		'word-wrap: break-word',
		'white-space: pre-wrap',
		'white-space: break-spaces',
		'-webkit-font-variant-ligatures: none',
		'font-variant-ligatures: none',
		'position: relative',
		'text-align: inherit',
		'padding: 0',
		'margin: 0',
	].join(';');
	const sticky = (data.props as { sticky?: boolean } | undefined)?.sticky;
	const stickyAttr = sticky ? ` ${STICKY_EXPORT_ATTR}="1"` : '';
	return `<div id="${data.id}" data-type="text"${stickyAttr} style="${style};"><div style="line-height: 1.4; letter-spacing: 0px; text-align: ${textAlign}; padding: 0; margin: 0;"><div style="${innerStyle}">${escaped}</div></div></div>`;
}

// ---------------------------------------------------------------------------
// toHTML serializers (mirrored from pdfdraft core registry.tsx)
// ---------------------------------------------------------------------------

interface ShapeProps {
	path?: string;
	viewBox?: [number, number];
	sticky?: boolean;
	borderRadius?: number;
	background?: { color?: string };
}

type ElementData = Parameters<ElementType['toHTML']>[0];
type HTMLSerializer = (data: ElementData) => string;

const generateTextHTML: HTMLSerializer = (data) => {
	const textProps = data.props as TextProps;
	const fontStyles = generateFontStyles(textProps?.globalStyle);
	const style = styleGenerator.generateElementStyle(data.style);
	const stickyAttr = textProps?.sticky ? ` ${STICKY_EXPORT_ATTR}="1"` : '';

	return `<div id="${data.id}" data-type="text"${stickyAttr} style="${style};">
    <div style="${COMMON_STYLES.fullSize};${COMMON_STYLES.lineHeight}; ${COMMON_STYLES.letterSpacing};${fontStyles}">
      <div style="${COMMON_STYLES.fullSize};${COMMON_STYLES.wordWrap};${COMMON_STYLES.fontVariant};position:relative;">
        ${data.content ?? ''}
      </div>
    </div>
  </div>`;
};

const generateImageHTML: HTMLSerializer = (data) => {
	const { flip, border, radius, src } = data.props || {};

	const scaleX = flip?.horizontal ? -1 : 1;
	const scaleY = flip?.vertical ? -1 : 1;
	const hasScale = scaleX !== 1 || scaleY !== 1;

	const imageStyles: string[] = [];

	if (border?.width !== undefined) {
		imageStyles.push(`border-width:${border.width}px`);
	}
	if (border?.style !== undefined) {
		imageStyles.push(`border-style:${border.style}`);
	}
	if (border?.color !== undefined) {
		imageStyles.push(`border-color:${border.color}`);
	}
	if (radius) {
		imageStyles.push(`border-radius:${radius}%`);
	}
	if (hasScale) {
		imageStyles.push(`transform:scale(${scaleX}, ${scaleY})`);
	}

	const imageStyleStr = imageStyles.length > 0 ? imageStyles.join('; ') : '';
	const style = styleGenerator.generateElementStyle(data.style);

	return `<div id="${data.id}" data-type="image" style="${style}"><img style="height:100%;width:100%;object-fit:cover;${COMMON_STYLES.block};${COMMON_STYLES.absolute};${imageStyleStr}" src="${src ?? placeholder}" /></div>`;
};

const generateDateHTML: HTMLSerializer = (data) => {
	const value = data.props?.value || new Date().toISOString().split('T')[0];
	const format = data.props?.format || 'yyyy-mm-dd';

	let display: string;
	switch (format) {
		case 'mm/dd/yyyy':
			display = new Date(value).toLocaleDateString('en-US');
			break;
		case 'dd-mm-yyyy':
			display = new Date(value)
				.toLocaleDateString('en-GB')
				.replace(/\//g, '-');
			break;
		default:
			display = new Date(value).toISOString().split('T')[0] as string;
	}

	const style = styleGenerator.generateElementStyle(data.style);
	return `<div id="${data.id}" style="${style};padding:4px 8px;white-space:nowrap;box-sizing:border-box">${display}</div>`;
};

const generateDividerHTML: HTMLSerializer = (data) => {
	const defaultBorder = data.props?.style?.['border-bottom'] as
		| string
		| undefined;
	const [defaultWeight = '10px', defaultType = 'solid'] = defaultBorder
		? defaultBorder.split(' ')
		: [];

	const weight =
		data.props?.lineWeight != null
			? `${data.props.lineWeight}px`
			: defaultWeight;
	const type = data.props?.lineType || defaultType;
	const color = data.props?.style?.color || '#000';
	// Width lives in data.style (element layout), not data.props.style
	const width = data.style?.width ?? data.props?.style?.width ?? '200px';

	const style = styleGenerator.generateElementStyle(data.style);
	return `<div id="${data.id}" style="${style};border-bottom:${weight} ${type} ${color};width:${width};height:0;box-sizing:border-box;cursor:pointer"></div>`;
};

const generateShapeHTML: HTMLSerializer = (data) => {
	const shapeProps = data.props as ShapeProps;
	const {
		path,
		viewBox,
		background = { color: DEFAULT_SHAPE_COLOR },
	} = shapeProps || {};

	if (!path || !viewBox) return '';

	const height = parseFloat(data.style.height || String(DEFAULT_SHAPE_SIZE));
	const width = parseFloat(data.style.width || String(DEFAULT_SHAPE_SIZE));
	const style = styleGenerator.generateElementStyle(data.style);
	const fillColor = background.color ?? DEFAULT_SHAPE_COLOR;

	const scaleX = width / viewBox[0];
	const scaleY = height / viewBox[1];

	const stickyAttr = shapeProps?.sticky ? ` ${STICKY_EXPORT_ATTR}="1"` : '';
	const borderRadius = shapeProps?.borderRadius ?? 0;
	const borderRadiusStyle =
		borderRadius > 0 ? `border-radius:${borderRadius}px;overflow:hidden` : '';

	return `<div id="${data.id}" data-type="shape"${stickyAttr} style="${style}${borderRadiusStyle ? `;${borderRadiusStyle}` : ''}">
    <svg height="${height}" width="${width}">
      <g transform="scale(${scaleX},${scaleY}) translate(0,0) matrix(1,0,0,1,0,0)">
        <path
          vector-effect="non-scaling-stroke"
          stroke-linecap="butt"
          stroke-miterlimit="8"
          d="${path}"
          fill="${fillColor}">
        </path>
      </g>
    </svg>
  </div>`;
};

const generateTableHTML: HTMLSerializer = (data) => {
	const props = data.props || {};
	const rows = props.rows ?? 3;
	const cols = props.cols ?? 3;
	const borderColor = props.style?.borderColor ?? '#000';
	const borderWidth = props.style?.borderWidth ?? '1px';
	const cellPadding = props.cellPadding ?? 8;
	const textAlign = props.style?.textAlign ?? 'left';

	const tableWidth = data.style?.width ?? props.style?.width ?? '300px';
	const tableWidthStr =
		typeof tableWidth === 'number' ? `${tableWidth}px` : tableWidth;
	const initialWidth = parseInt(tableWidthStr, 10);
	const columnWidths =
		props.columnWidths ?? Array(cols).fill(initialWidth / cols);
	const rowHeights = props.rowHeights ?? Array(rows).fill('auto');
	const contents: { content: string; json: any }[][] = props.contents ?? [];
	const defaultHeaders = Array.from(
		{ length: cols },
		(_, i) => `h-${i + 1}`,
	);

	const style = styleGenerator.generateElementStyle(data.style);
	const tableHeight = (data.style?.height ?? props.style?.height)
		? `height:${typeof (data.style?.height ?? props.style?.height) === 'number' ? `${data.style?.height ?? props.style?.height}px` : (data.style?.height ?? props.style?.height)}`
		: '';

	const parts: string[] = [
		`<table id="${data.id}" style="${style};border-collapse:collapse;width:${tableWidthStr};${tableHeight}">`,
	];

	parts.push('<colgroup>');
	columnWidths.forEach((w: number) =>
		parts.push(`<col style="width:${w}px" />`),
	);
	parts.push('</colgroup><tbody>');

	for (let r = 0; r < rows; r++) {
		const rowH = rowHeights[r];
		const rowCss =
			typeof rowH === 'number' ? `height:${rowH}px` : `height:${rowH}`;
		parts.push(`<tr style="${rowCss}">`);

		for (let c = 0; c < cols; c++) {
			const isHeader = r === 0;
			const raw = contents[r]?.[c];
			const cellContent =
				raw?.content != null
					? raw.content
					: isHeader
						? defaultHeaders[c]
						: '';
			const tag = isHeader ? 'th' : 'td';
			const cellCss = `border:${borderWidth} solid ${borderColor};padding:${cellPadding}px;text-align:${textAlign};position:relative`;

			parts.push(
				`<${tag} style="${cellCss}"><div style="width:100%;height:100%;display:block;min-height:20px;word-wrap:break-word;white-space:pre-wrap;white-space:break-spaces;">${cellContent}</div></${tag}>`,
			);
		}

		parts.push('</tr>');
	}

	parts.push('</tbody></table>');
	return parts.join('');
};

const generateChartHTML: HTMLSerializer = (data) => {
	const chartData = data.props?.chartData || {
		type: 'bar',
		data: {
			labels: ['Q1', 'Q2', 'Q3', 'Q4'],
			datasets: [
				{
					label: 'Sales',
					data: [65, 59, 80, 81],
					backgroundColor: '#4299e1',
					borderColor: '#2b6cb0',
					borderWidth: 1,
				},
			],
		},
		options: {
			responsive: true,
			maintainAspectRatio: false,
			plugins: { legend: { position: 'top', display: true } },
		},
	};

	const widthPx = parseInt(data.props?.width || '600px', 10);
	const heightPx = parseInt(data.props?.height || '400px', 10);
	const margin = { top: 20, right: 20, bottom: 40, left: 40 };
	const chartWidth = widthPx - margin.left - margin.right;
	const chartHeight = heightPx - margin.top - margin.bottom;

	const labels = chartData.data.labels || [];
	const dataset = chartData.data.datasets[0] || {
		data: [],
		backgroundColor: '#4299e1',
		borderColor: '#2b6cb0',
		borderWidth: 1,
	};
	const values: number[] = dataset.data || [];
	const maxValue = Math.max(...values, 1);

	let chartContent = '';

	const generateYGrid = (tickValues: number[]) =>
		tickValues
			.map((value: number) => {
				const y =
					margin.top + chartHeight - (value / maxValue) * chartHeight;
				return `<line x1="${margin.left}" y1="${y.toFixed(2)}" x2="${margin.left + chartHeight}" y2="${y.toFixed(2)}" stroke="#e0e0e0" stroke-width="1" /><text x="${margin.left - 5}" y="${(y + 4).toFixed(2)}" text-anchor="end" font-size="12" fill="#666">${value}</text>`;
			})
			.join('');

	const generateXLabels = (xPositions: number[]) =>
		labels
			.map(
				(label: string, i: number) =>
					`<text x="${xPositions[i]!.toFixed(2)}" y="${(margin.top + chartHeight + 16).toFixed(2)}" text-anchor="middle" font-size="12" fill="#666">${label}</text>`,
			)
			.join('');

	const numTicks = 5;
	const tickValues = Array.from({ length: numTicks }, (_, i) =>
		Math.round((maxValue / (numTicks - 1)) * i),
	);

	switch (chartData.type) {
		case 'bar': {
			const barCount = labels.length;
			const barSpacing = (chartWidth * 0.1) / barCount;
			const totalBarSpacing = barSpacing * (barCount + 1);
			const singleBarWidth = (chartWidth - totalBarSpacing) / barCount;

			const bars = labels
				.map((_: string, i: number) => {
					const v = values[i] || 0;
					const barHeight = (v / maxValue) * chartHeight;
					const x =
						margin.left +
						barSpacing +
						i * (singleBarWidth + barSpacing);
					const y = margin.top + (chartHeight - barHeight);
					return `<rect x="${x.toFixed(2)}" y="${y.toFixed(2)}" width="${singleBarWidth.toFixed(2)}" height="${barHeight.toFixed(2)}" fill="${dataset.backgroundColor || '#4299e1'}" stroke="${dataset.borderColor || '#2b6cb0'}" stroke-width="${dataset.borderWidth || 1}" />`;
				})
				.join('');

			const xPositions = labels.map(
				(_: string, i: number) =>
					margin.left +
					barSpacing +
					i * (singleBarWidth + barSpacing) +
					singleBarWidth / 2,
			);
			chartContent = `<svg width="${widthPx}" height="${heightPx}">
        <line x1="${margin.left}" y1="${margin.top}" x2="${margin.left}" y2="${margin.top + chartHeight}" stroke="#000" stroke-width="1" />
        <line x1="${margin.left}" y1="${margin.top + chartHeight}" x2="${margin.left + chartWidth}" y2="${margin.top + chartHeight}" stroke="#000" stroke-width="1" />
        ${generateYGrid(tickValues)}${bars}${generateXLabels(xPositions)}
      </svg>`;
			break;
		}
		case 'line': {
			const points = labels.map((_: string, i: number) => {
				const v = values[i] || 0;
				const x =
					margin.left + (i / (labels.length - 1)) * chartWidth;
				const y =
					margin.top +
					chartHeight -
					(v / maxValue) * chartHeight;
				return { x, y };
			});

			const polylinePoints = points
				.map(
					(p: { x: number; y: number }) =>
						`${p.x.toFixed(2)},${p.y.toFixed(2)}`,
				)
				.join(' ');
			const circles = points
				.map(
					(p: { x: number; y: number }) =>
						`<circle cx="${p.x.toFixed(2)}" cy="${p.y.toFixed(2)}" r="3" fill="${dataset.backgroundColor || '#4299e1'}" stroke="${dataset.borderColor || '#2b6cb0'}" stroke-width="${dataset.borderWidth || 1}" />`,
				)
				.join('');

			const xPositions = labels.map(
				(_: string, i: number) =>
					margin.left + (i / (labels.length - 1)) * chartWidth,
			);
			chartContent = `<svg width="${widthPx}" height="${heightPx}">
        <line x1="${margin.left}" y1="${margin.top}" x2="${margin.left}" y2="${margin.top + chartHeight}" stroke="#000" stroke-width="1" />
        <line x1="${margin.left}" y1="${margin.top + chartHeight}" x2="${margin.left + chartWidth}" y2="${margin.top + chartHeight}" stroke="#000" stroke-width="1" />
        ${generateYGrid(tickValues)}
        <polyline points="${polylinePoints}" fill="none" stroke="${dataset.borderColor || '#4299e1'}" stroke-width="${dataset.borderWidth || 2}" />
        ${circles}${generateXLabels(labels.map((_: string, i: number) => margin.left + (i / (labels.length - 1)) * chartWidth))}
      </svg>`;
			break;
		}
		case 'pie':
		case 'doughnut': {
			const total =
				values.reduce((sum: number, v: number) => sum + v, 0) || 1;
			const centerX = widthPx / 2;
			const centerY = heightPx / 2;
			const radius = Math.min(centerX, centerY) - 20;
			const innerRadius =
				chartData.type === 'doughnut' ? radius * 0.6 : 0;
			const colors = [
				'#4299e1',
				'#48bb78',
				'#ed8936',
				'#9f7aea',
				'#38b2ac',
			];
			let currentAngle = 0;

			const slices = values
				.map((value: number, i: number) => {
					const sliceAngle = (value / total) * 360;
					const startAngle = currentAngle;
					const endAngle = currentAngle + sliceAngle;
					currentAngle += sliceAngle;
					const startRad = (startAngle * Math.PI) / 180;
					const endRad = (endAngle * Math.PI) / 180;
					const x1 = centerX + radius * Math.cos(startRad);
					const y1 = centerY + radius * Math.sin(startRad);
					const x2 = centerX + radius * Math.cos(endRad);
					const y2 = centerY + radius * Math.sin(endRad);
					const largeArc = sliceAngle > 180 ? 1 : 0;
					const pathData = `M ${centerX.toFixed(2)} ${centerY.toFixed(2)} L ${x1.toFixed(2)} ${y1.toFixed(2)} A ${radius.toFixed(2)} ${radius.toFixed(2)} 0 ${largeArc} 1 ${x2.toFixed(2)} ${y2.toFixed(2)} Z`;
					return `<path d="${pathData}" fill="${colors[i % colors.length]}" stroke="#fff" stroke-width="2"/>`;
				})
				.join('');

			const innerCircle =
				chartData.type === 'doughnut'
					? `<circle cx="${centerX}" cy="${centerY}" r="${innerRadius}" fill="white"/>`
					: '';
			chartContent = `<svg width="${widthPx}" height="${heightPx}">${slices}${innerCircle}</svg>`;
			break;
		}
		default:
			chartContent = '';
	}

	// Legend
	const showLegend =
		chartData.options?.plugins?.legend?.display !== false;
	const legendPosition =
		chartData.options?.plugins?.legend?.position || 'top';
	let legendContent = '';
	if (showLegend && chartData.data.datasets.length > 0) {
		const items = chartData.data.datasets
			.map((ds: any, idx: number) => {
				const color = ds.backgroundColor || '#4299e1';
				return `<div style="display: flex; align-items: center; gap: 4px; margin: 2px;"><span style="display: inline-block;width: 12px;height: 12px;background-color: ${color};border-radius: 2px;"></span><span style="font-size: 12px; color: #333;">${ds.label || `Dataset ${idx + 1}`}</span></div>`;
			})
			.join('');

		const legendStyle =
			legendPosition === 'top' || legendPosition === 'bottom'
				? 'display: flex; justify-content: center; gap: 12px; flex-wrap: wrap; padding: 8px;'
				: 'display: flex; flex-direction: column; gap: 4px; padding: 8px;';
		legendContent = `<div style="${legendStyle}">${items}</div>`;
	}

	const style = styleGenerator.generateElementStyle(data.style);
	const containerStyle = `${style};width: ${widthPx}px;height: ${heightPx}px;background: white;border: 1px solid #e2e8f0;border-radius: 8px;overflow: hidden;display: flex;flex-direction: ${legendPosition === 'top' || legendPosition === 'bottom' ? 'column' : 'row'}`;

	const topLegend = legendPosition === 'top' ? legendContent : '';
	const bottomLegend = legendPosition === 'bottom' ? legendContent : '';
	const sideLegend =
		legendPosition === 'left' || legendPosition === 'right'
			? legendContent
			: '';

	return `<div id="${data.id}" style="${containerStyle}">${topLegend}<div style="flex: 1; display: flex; align-items: center; justify-content: center;">${chartContent}</div>${bottomLegend}${sideLegend}</div>`;
};

// ---------------------------------------------------------------------------
// Minimal ElementType factory (no React components needed)
// ---------------------------------------------------------------------------

function makeSerializerElement(
	namespace: string,
	toHTML: HTMLSerializer,
): ElementType {
	return {
		namespace,
		label: namespace,
		category: 'core',
		render: (() => null) as any,
		toHTML,
	};
}

// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------

let registered = false;

/**
 * Populates the element store with lightweight toHTML-only entries so
 * HTMLGenerator can serialise every element type to HTML strings.
 *
 * Safe to call multiple times — subsequent calls are no-ops.
 */
export function registerHtmlSerializers(): void {
	if (registered) return;
	registered = true;

	const { register } = useElementsStore.getState().actions;

	const serializers: [string, HTMLSerializer][] = [
		['text', generateTextHTML],
		['image', generateImageHTML],
		['date', generateDateHTML],
		['divider', generateDividerHTML],
		['shape', generateShapeHTML],
		['table', generateTableHTML],
		['chart', generateChartHTML],
		['duration', () => ''],
		// WooCommerce merge-tag element
		['woocommerce', advancedSelectorToHTML],
		// WooCommerce cart/product table element
		[PRODUCT_TABLE_NAMESPACE, productTableToHTML],
		// WooCommerce order totals block element
		[TOTAL_BLOCK_NAMESPACE, totalBlockToHTML],
	];

	for (const [ns, fn] of serializers) {
		register(makeSerializerElement(ns, fn));
	}
}
