import {
	getShaderCompilationDiagnostics,
	type ShaderCompilationDiagnostic
} from './error-diagnostics.js';
import { formatShaderSourceLocation } from './shader.js';

/**
 * Runtime phase in which an error occurred.
 */
export type MotionGPUErrorPhase = 'initialization' | 'render';

/**
 * Stable machine-readable error category code.
 */
export type MotionGPUErrorCode =
	| 'WEBGPU_UNAVAILABLE'
	| 'WEBGPU_ADAPTER_UNAVAILABLE'
	| 'WEBGPU_CONTEXT_UNAVAILABLE'
	| 'WGSL_COMPILATION_FAILED'
	| 'MATERIAL_PREPROCESS_FAILED'
	| 'WEBGPU_DEVICE_LOST'
	| 'WEBGPU_UNCAPTURED_ERROR'
	| 'BIND_GROUP_MISMATCH'
	| 'RUNTIME_RESOURCE_MISSING'
	| 'UNIFORM_VALUE_INVALID'
	| 'STORAGE_BUFFER_OUT_OF_BOUNDS'
	| 'STORAGE_BUFFER_READ_FAILED'
	| 'RENDER_GRAPH_INVALID'
	| 'PINGPONG_CONFIGURATION_INVALID'
	| 'TEXTURE_USAGE_INVALID'
	| 'TEXTURE_REQUEST_FAILED'
	| 'TEXTURE_DECODE_UNAVAILABLE'
	| 'TEXTURE_REQUEST_ABORTED'
	| 'COMPUTE_COMPILATION_FAILED'
	| 'COMPUTE_CONTRACT_INVALID'
	| 'MOTIONGPU_RUNTIME_ERROR';

/**
 * Severity level for user-facing diagnostics.
 */
export type MotionGPUErrorSeverity = 'error' | 'fatal';

/**
 * One source-code line displayed in diagnostics snippet.
 */
export interface MotionGPUErrorSourceLine {
	number: number;
	code: string;
	highlight: boolean;
}

/**
 * Structured source context displayed for shader compilation errors.
 */
export interface MotionGPUErrorSource {
	component: string;
	location: string;
	line: number;
	column?: number;
	snippet: MotionGPUErrorSourceLine[];
}

/**
 * Optional runtime context captured with diagnostics payload.
 */
export interface MotionGPUErrorContext {
	materialSignature?: string;
	passGraph?: {
		passCount: number;
		enabledPassCount: number;
		inputs: string[];
		outputs: string[];
	};
	activeRenderTargets: string[];
}

/**
 * Structured error payload used by UI diagnostics.
 */
export interface MotionGPUErrorReport {
	/**
	 * Stable machine-readable category code.
	 */
	code: MotionGPUErrorCode;
	/**
	 * Severity level used by diagnostics UIs and telemetry.
	 */
	severity: MotionGPUErrorSeverity;
	/**
	 * Whether runtime may recover without full renderer re-creation.
	 */
	recoverable: boolean;
	/**
	 * Short category title.
	 */
	title: string;
	/**
	 * Primary human-readable message.
	 */
	message: string;
	/**
	 * Suggested remediation hint.
	 */
	hint: string;
	/**
	 * Additional parsed details (for example WGSL line errors).
	 */
	details: string[];
	/**
	 * Stack trace lines when available.
	 */
	stack: string[];
	/**
	 * Original unmodified message.
	 */
	rawMessage: string;
	/**
	 * Runtime phase where the error occurred.
	 */
	phase: MotionGPUErrorPhase;
	/**
	 * Optional source context for shader-related diagnostics.
	 */
	source: MotionGPUErrorSource | null;
	/**
	 * Optional runtime context snapshot (material/pass graph/render targets).
	 */
	context: MotionGPUErrorContext | null;
}

/**
 * Splits multi-line values into trimmed non-empty lines.
 */
function splitLines(value: string): string[] {
	return value
		.split('\n')
		.map((line) => line.trim())
		.filter((line) => line.length > 0);
}

function toDisplayName(path: string): string {
	const normalized = path.split(/[?#]/)[0] ?? path;
	const chunks = normalized.split(/[\\/]/);
	const last = chunks[chunks.length - 1];
	return last && last.length > 0 ? last : path;
}

function toSnippet(source: string, line: number, radius = 3): MotionGPUErrorSourceLine[] {
	const lines = source.replace(/\r\n?/g, '\n').split('\n');
	if (lines.length === 0) {
		return [];
	}

	const targetLine = Math.min(Math.max(1, line), lines.length);
	const start = Math.max(1, targetLine - radius);
	const end = Math.min(lines.length, targetLine + radius);
	const snippet: MotionGPUErrorSourceLine[] = [];

	for (let index = start; index <= end; index += 1) {
		snippet.push({
			number: index,
			code: lines[index - 1] ?? '',
			highlight: index === targetLine
		});
	}

	return snippet;
}

function buildSourceFromDiagnostics(error: unknown): MotionGPUErrorSource | null {
	const diagnostics = getShaderCompilationDiagnostics(error);
	if (!diagnostics || diagnostics.diagnostics.length === 0) {
		return null;
	}

	const primary = diagnostics.diagnostics.find((entry) => entry.sourceLocation !== null);
	if (!primary?.sourceLocation) {
		return null;
	}

	const location = primary.sourceLocation;
	const column = primary.linePos && primary.linePos > 0 ? primary.linePos : undefined;

	if (location.kind === 'fragment') {
		const component =
			diagnostics.materialSource?.component ??
			(diagnostics.materialSource?.file
				? toDisplayName(diagnostics.materialSource.file)
				: 'User shader fragment');
		const locationLabel = formatShaderSourceLocation(location) ?? `fragment line ${location.line}`;
		return {
			component,
			location: `${component} (${locationLabel})`,
			line: location.line,
			...(column !== undefined ? { column } : {}),
			snippet: toSnippet(diagnostics.fragmentSource, location.line)
		};
	}

	if (location.kind === 'include') {
		const includeName = location.include ?? 'unknown';
		const includeSource = diagnostics.includeSources[includeName] ?? '';
		const component = `#include <${includeName}>`;
		const locationLabel = formatShaderSourceLocation(location) ?? `include <${includeName}>`;
		return {
			component,
			location: `${component} (${locationLabel})`,
			line: location.line,
			...(column !== undefined ? { column } : {}),
			snippet: toSnippet(includeSource, location.line)
		};
	}

	if (location.kind === 'compute') {
		const computeSource = diagnostics.computeSource ?? diagnostics.fragmentSource;
		const component = 'Compute shader';
		const locationLabel = formatShaderSourceLocation(location) ?? `compute line ${location.line}`;
		return {
			component,
			location: `${component} (${locationLabel})`,
			line: location.line,
			...(column !== undefined ? { column } : {}),
			snippet: toSnippet(computeSource, location.line)
		};
	}

	const defineName = location.define ?? 'unknown';
	const defineLine = Math.max(1, location.line);
	const component = `#define ${defineName}`;
	const locationLabel =
		formatShaderSourceLocation(location) ?? `define "${defineName}" line ${defineLine}`;
	return {
		component,
		location: `${component} (${locationLabel})`,
		line: defineLine,
		...(column !== undefined ? { column } : {}),
		snippet: toSnippet(diagnostics.defineBlockSource ?? '', defineLine, 2)
	};
}

function formatDiagnosticMessage(entry: ShaderCompilationDiagnostic): string {
	const sourceLabel = formatShaderSourceLocation(entry.sourceLocation);
	const generatedLineLabel =
		entry.generatedLine > 0 ? `generated WGSL line ${entry.generatedLine}` : null;
	const labels = [sourceLabel, generatedLineLabel].filter((value) => Boolean(value));
	if (labels.length === 0) {
		return entry.message;
	}

	return `[${labels.join(' | ')}] ${entry.message}`;
}

/**
 * Maps known WebGPU/WGSL error patterns to a user-facing title and hint.
 */
function classifyErrorMessage(
	message: string
): Pick<MotionGPUErrorReport, 'code' | 'severity' | 'recoverable' | 'title' | 'hint'> {
	if (message.includes('WebGPU is not available in this browser')) {
		return {
			code: 'WEBGPU_UNAVAILABLE',
			severity: 'fatal',
			recoverable: false,
			title: 'WebGPU unavailable',
			hint: 'Use a browser with WebGPU enabled (latest Chrome/Edge/Safari TP) and secure context.'
		};
	}

	if (message.includes('Unable to acquire WebGPU adapter')) {
		return {
			code: 'WEBGPU_ADAPTER_UNAVAILABLE',
			severity: 'fatal',
			recoverable: false,
			title: 'WebGPU adapter unavailable',
			hint: 'GPU adapter request failed. Check browser permissions, flags and device support.'
		};
	}

	if (message.includes('Canvas does not support webgpu context')) {
		return {
			code: 'WEBGPU_CONTEXT_UNAVAILABLE',
			severity: 'error',
			recoverable: true,
			title: 'Canvas cannot create WebGPU context',
			hint: 'Make sure this canvas is attached to DOM and not using an unsupported context option.'
		};
	}

	if (message.includes('WGSL compilation failed')) {
		return {
			code: 'WGSL_COMPILATION_FAILED',
			severity: 'error',
			recoverable: true,
			title: 'WGSL compilation failed',
			hint: 'Check WGSL line numbers below and verify struct/binding/function signatures.'
		};
	}

	if (
		message.includes('Invalid include directive in fragment shader.') ||
		message.includes('Unknown include "') ||
		message.includes('Circular include detected for "') ||
		message.includes('Invalid define value for "') ||
		message.includes('Invalid include "')
	) {
		return {
			code: 'MATERIAL_PREPROCESS_FAILED',
			severity: 'error',
			recoverable: true,
			title: 'Material preprocess failed',
			hint: 'Validate #include keys, define values and include expansion order before retrying.'
		};
	}

	if (message.includes('Compute shader compilation failed')) {
		return {
			code: 'COMPUTE_COMPILATION_FAILED',
			severity: 'error',
			recoverable: true,
			title: 'Compute shader compilation failed',
			hint: 'Check WGSL compute shader sources below and verify storage bindings.'
		};
	}

	if (
		message.includes(
			'Compute shader must declare `@compute @workgroup_size(...) fn compute(...)`.'
		) ||
		message.includes('Compute shader must include a `@builtin(global_invocation_id)` parameter.') ||
		message.includes('Could not extract @workgroup_size from compute shader source.') ||
		message.includes('@workgroup_size dimensions must be integers in range') ||
		message.includes('Unsupported storage buffer access mode "')
	) {
		return {
			code: 'COMPUTE_CONTRACT_INVALID',
			severity: 'error',
			recoverable: true,
			title: 'Compute contract is invalid',
			hint: 'Ensure compute shader contract (@compute, @workgroup_size, global_invocation_id, storage access) is valid.'
		};
	}

	if (message.includes('WebGPU device lost') || message.includes('Device Lost')) {
		return {
			code: 'WEBGPU_DEVICE_LOST',
			severity: 'fatal',
			recoverable: false,
			title: 'WebGPU device lost',
			hint: 'GPU device/context was lost. Recreate the renderer and check OS/GPU stability.'
		};
	}

	if (
		message.includes('Dispatch workgroup count') &&
		message.includes('max compute workgroups per dimension')
	) {
		return {
			code: 'WEBGPU_UNCAPTURED_ERROR',
			severity: 'error',
			recoverable: true,
			title: 'Compute dispatch exceeds device limit',
			hint: 'Reduce dispatch counts or split compute work into multiple dispatches/chunks.'
		};
	}

	if (
		message.includes('maximum storage buffer binding size') ||
		message.includes('maxStorageBufferBindingSize')
	) {
		return {
			code: 'WEBGPU_UNCAPTURED_ERROR',
			severity: 'error',
			recoverable: true,
			title: 'Storage buffer exceeds binding limit',
			hint: 'Keep each storage buffer binding below adapter limits or shard data across multiple buffers.'
		};
	}

	if (message.includes('WebGPU uncaptured error')) {
		return {
			code: 'WEBGPU_UNCAPTURED_ERROR',
			severity: 'error',
			recoverable: true,
			title: 'WebGPU uncaptured error',
			hint: 'A GPU command failed asynchronously. Review details and validate resource/state usage.'
		};
	}

	if (message.includes('CreateBindGroup') || message.includes('bind group layout')) {
		return {
			code: 'BIND_GROUP_MISMATCH',
			severity: 'error',
			recoverable: true,
			title: 'Bind group mismatch',
			hint: 'Bindings in shader and runtime resources are out of sync. Verify uniforms/textures layout.'
		};
	}

	if (message.includes('Storage buffer "') && message.includes('write out of bounds:')) {
		return {
			code: 'STORAGE_BUFFER_OUT_OF_BOUNDS',
			severity: 'error',
			recoverable: true,
			title: 'Storage buffer write out of bounds',
			hint: 'Ensure offset + write byte length does not exceed declared storage buffer size.'
		};
	}

	if (
		message.includes('Cannot read storage buffer "') ||
		message.includes('Cannot read storage buffer: GPU device unavailable.') ||
		message.includes('not allocated on GPU.')
	) {
		return {
			code: 'STORAGE_BUFFER_READ_FAILED',
			severity: 'error',
			recoverable: true,
			title: 'Storage buffer read failed',
			hint: 'Readbacks require an initialized renderer, allocated GPU buffer and active device.'
		};
	}

	if (
		message.includes('Unknown uniform "') ||
		message.includes('Unknown uniform type for "') ||
		message.includes('Unknown texture "') ||
		message.includes('Unknown storage buffer "') ||
		message.includes('Missing definition for storage buffer "') ||
		message.includes('Missing texture definition for "') ||
		(message.includes('Storage buffer "') && message.includes('" not allocated.')) ||
		(message.includes('Storage texture "') && message.includes('" not allocated.'))
	) {
		return {
			code: 'RUNTIME_RESOURCE_MISSING',
			severity: 'error',
			recoverable: true,
			title: 'Runtime resource binding failed',
			hint: 'Check material declarations and runtime keys for uniforms, textures and storage resources.'
		};
	}

	if (message.includes('Uniform ') && message.includes(' value must')) {
		return {
			code: 'UNIFORM_VALUE_INVALID',
			severity: 'error',
			recoverable: true,
			title: 'Uniform value is invalid',
			hint: 'Provide finite values with tuple/matrix sizes matching the uniform type.'
		};
	}

	if (
		message.includes('Render pass #') ||
		message.includes('Render graph references unknown runtime target')
	) {
		return {
			code: 'RENDER_GRAPH_INVALID',
			severity: 'error',
			recoverable: true,
			title: 'Render graph configuration is invalid',
			hint: 'Verify pass inputs/outputs, declared render targets and execution order.'
		};
	}

	if (message.includes('PingPongComputePass must provide a target texture key.')) {
		return {
			code: 'PINGPONG_CONFIGURATION_INVALID',
			severity: 'error',
			recoverable: true,
			title: 'Ping-pong compute pass is misconfigured',
			hint: 'Configure a valid target texture key for PingPongComputePass.'
		};
	}

	if (message.includes('Destination texture needs to have CopyDst')) {
		return {
			code: 'TEXTURE_USAGE_INVALID',
			severity: 'error',
			recoverable: true,
			title: 'Invalid texture usage flags',
			hint: 'Texture used as upload destination must include CopyDst (and often RenderAttachment).'
		};
	}

	if (message.includes('Texture request failed')) {
		return {
			code: 'TEXTURE_REQUEST_FAILED',
			severity: 'error',
			recoverable: true,
			title: 'Texture request failed',
			hint: 'Verify texture URL, CORS policy and response status before retrying.'
		};
	}

	if (message.includes('createImageBitmap is not available in this runtime')) {
		return {
			code: 'TEXTURE_DECODE_UNAVAILABLE',
			severity: 'fatal',
			recoverable: false,
			title: 'Texture decode unavailable',
			hint: 'Runtime lacks createImageBitmap support. Use a browser/runtime with image bitmap decoding.'
		};
	}

	if (message.toLowerCase().includes('texture request was aborted')) {
		return {
			code: 'TEXTURE_REQUEST_ABORTED',
			severity: 'error',
			recoverable: true,
			title: 'Texture request aborted',
			hint: 'Texture load was cancelled. Retry the request when source inputs stabilize.'
		};
	}

	return {
		code: 'MOTIONGPU_RUNTIME_ERROR',
		severity: 'error',
		recoverable: true,
		title: 'MotionGPU render error',
		hint: 'Review technical details below. If issue persists, isolate shader/uniform/texture changes.'
	};
}

/**
 * Converts unknown errors to a consistent, display-ready error report.
 *
 * @param error - Unknown thrown value.
 * @param phase - Phase during which error occurred.
 * @returns Normalized error report.
 */
export function toMotionGPUErrorReport(
	error: unknown,
	phase: MotionGPUErrorPhase
): MotionGPUErrorReport {
	const shaderDiagnostics = getShaderCompilationDiagnostics(error);
	const rawMessage =
		error instanceof Error
			? error.message
			: typeof error === 'string'
				? error
				: 'Unknown FragCanvas error';
	const rawLines = splitLines(rawMessage);
	const defaultMessage = rawLines[0] ?? rawMessage;
	const defaultDetails = rawLines.slice(1);
	const source = buildSourceFromDiagnostics(error);
	const context = shaderDiagnostics?.runtimeContext ?? null;
	const message =
		shaderDiagnostics && shaderDiagnostics.diagnostics[0]
			? formatDiagnosticMessage(shaderDiagnostics.diagnostics[0])
			: defaultMessage;
	const details = shaderDiagnostics
		? shaderDiagnostics.diagnostics.slice(1).map((entry) => formatDiagnosticMessage(entry))
		: defaultDetails;
	const stack =
		error instanceof Error && error.stack
			? splitLines(error.stack).filter((line) => line !== message)
			: [];
	let classification = classifyErrorMessage(rawMessage);
	if (
		shaderDiagnostics?.shaderStage === 'compute' &&
		classification.code === 'WGSL_COMPILATION_FAILED'
	) {
		classification = {
			code: 'COMPUTE_COMPILATION_FAILED',
			severity: 'error',
			recoverable: true,
			title: 'Compute shader compilation failed',
			hint: 'Check WGSL compute shader sources below and verify storage bindings.'
		};
	}

	return {
		code: classification.code,
		severity: classification.severity,
		recoverable: classification.recoverable,
		title: classification.title,
		message,
		hint: classification.hint,
		details,
		stack,
		rawMessage,
		phase,
		source,
		context
	};
}
