// Image processing utilities

/** The one refusal signal: the browser could not decode the uploaded file. */
export class UndecodableImageError extends Error {
	constructor( message = 'Image could not be decoded' ) {
		super( message );
		this.name = 'UndecodableImageError';
	}
}

export interface EncodeResult {
	dataUrl: string;
	bytes: number;
}

export interface LoadedImage {
	width: number;
	height: number;
	hasAlpha: boolean;
	encode: (
		width: number,
		height: number,
		format: 'jpeg' | 'png',
		quality: number
	) => EncodeResult;
}

export type ImageLoader = ( imageData: string ) => Promise<LoadedImage>;

export interface CompressOptions {
	maxWidth: number;
	maxHeight: number;
	targetBytes: number;
	startQuality?: number;
	minQuality?: number;
	qualityStep?: number;
	minDimension?: number;
	dimensionStep?: number;
}

/** Byte length of the payload encoded in a base64 data URL. */
export function base64ByteLength( dataUrl: string ): number {
	const comma = dataUrl.indexOf( ',' );
	const b64 = comma >= 0 ? dataUrl.slice( comma + 1 ) : dataUrl;
	const padding = b64.endsWith( '==' ) ? 2 : b64.endsWith( '=' ) ? 1 : 0;
	return Math.floor( ( b64.length * 3 ) / 4 ) - padding;
}

function qualitiesFrom( start: number, min: number, step: number ): number[] {
	const qualities: number[] = [];
	for ( let q = start; q > min + 1e-9; q -= step ) {
		qualities.push( Number( q.toFixed( 2 ) ) );
	}
	qualities.push( min );
	return qualities;
}

/**
 * Normalize an image to a target byte size without ever refusing for size.
 * Downscale to fit maxWidth/maxHeight, then (JPEG only) step quality down, then
 * shrink dimensions toward minDimension. At the floor, return best-effort.
 * Rejects only with UndecodableImageError (via the loader).
 */
export async function compressToTarget(
	imageData: string,
	opts: CompressOptions,
	loader: ImageLoader
): Promise<string> {
	const {
		maxWidth,
		maxHeight,
		targetBytes,
		startQuality = 0.85,
		minQuality = 0.6,
		qualityStep = 0.1,
		minDimension = 512,
		dimensionStep = 0.85,
	} = opts;

	const img = await loader( imageData );
	const format: 'jpeg' | 'png' = img.hasAlpha ? 'png' : 'jpeg';
	const qualities = format === 'jpeg' ? qualitiesFrom( startQuality, minQuality, qualityStep ) : [ 1 ];

	// Fit within the max box; only ever downscale (never upscale).
	const capRatio = Math.min( 1, maxWidth / img.width, maxHeight / img.height );
	const baseWidth = Math.max( 1, Math.round( img.width * capRatio ) );
	const baseHeight = Math.max( 1, Math.round( img.height * capRatio ) );
	const baseLongEdge = Math.max( baseWidth, baseHeight );

	let best: EncodeResult | null = null;
	let scale = 1;

	for ( ;; ) {
		const width = Math.max( 1, Math.round( baseWidth * scale ) );
		const height = Math.max( 1, Math.round( baseHeight * scale ) );

		for ( const quality of qualities ) {
			best = img.encode( width, height, format, quality );
			if ( best.bytes <= targetBytes ) {
				return best.dataUrl;
			}
		}

		if ( Math.max( width, height ) <= minDimension ) {
			break; // dimension floor reached
		}

		scale *= dimensionStep;
		// Clamp so the long edge lands exactly on the floor, not below it.
		if ( Math.round( baseLongEdge * scale ) < minDimension ) {
			scale = minDimension / baseLongEdge;
		}
	}

	// Never refuse for size: return the smallest encoding produced.
	return best!.dataUrl;
}

/** True if the decoded image has any non-opaque pixel. JPEG never has alpha. */
function detectAlpha( img: HTMLImageElement, dataUrl: string, width: number, height: number ): boolean {
	if ( dataUrl.startsWith( 'data:image/jpeg' ) || dataUrl.startsWith( 'data:image/jpg' ) ) {
		return false;
	}
	const sampleW = Math.max( 1, Math.min( width, 64 ) );
	const sampleH = Math.max( 1, Math.min( height, 64 ) );
	const canvas = document.createElement( 'canvas' );
	canvas.width = sampleW;
	canvas.height = sampleH;
	const ctx = canvas.getContext( '2d' );
	if ( ! ctx ) {
		return false;
	}
	ctx.drawImage( img, 0, 0, sampleW, sampleH );
	const { data } = ctx.getImageData( 0, 0, sampleW, sampleH );
	for ( let i = 3; i < data.length; i += 4 ) {
		if ( data[ i ] < 255 ) {
			return true;
		}
	}
	return false;
}

/** Real-browser loader: decode via <img>, encode via canvas. */
export const browserImageLoader: ImageLoader = ( imageData ) =>
	new Promise( ( resolve, reject ) => {
		const img = new Image();
		img.onload = () => {
			const width = img.naturalWidth || img.width;
			const height = img.naturalHeight || img.height;
			resolve( {
				width,
				height,
				hasAlpha: detectAlpha( img, imageData, width, height ),
				encode: ( w, h, format, quality ) => {
					const canvas = document.createElement( 'canvas' );
					canvas.width = w;
					canvas.height = h;
					const ctx = canvas.getContext( '2d' );
					if ( ! ctx ) {
						throw new UndecodableImageError( 'Canvas 2D context unavailable' );
					}
					ctx.drawImage( img, 0, 0, w, h );
					const mime = format === 'png' ? 'image/png' : 'image/jpeg';
					const dataUrl = canvas.toDataURL( mime, quality );
					return { dataUrl, bytes: base64ByteLength( dataUrl ) };
				},
			} );
		};
		img.onerror = () => reject( new UndecodableImageError() );
		img.src = imageData;
	} );

const AVATAR_OPTS: CompressOptions = {
	maxWidth: 768,
	maxHeight: 1024,
	targetBytes: 1_500_000,
	startQuality: 0.85,
	minQuality: 0.6,
	minDimension: 512,
};

const GARMENT_OPTS: CompressOptions = {
	maxWidth: 1024,
	maxHeight: 1024,
	targetBytes: 1_500_000,
	startQuality: 0.85,
	minQuality: 0.6,
	minDimension: 512,
};

export function resizeAvatarImage( imageData: string, loader: ImageLoader = browserImageLoader ): Promise<string> {
	return compressToTarget( imageData, AVATAR_OPTS, loader );
}

export function compressGarmentImage( imageData: string, loader: ImageLoader = browserImageLoader ): Promise<string> {
	return compressToTarget( imageData, GARMENT_OPTS, loader );
}

export function fileToBase64(file: File, abortSignal?: AbortSignal): Promise<string> {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();

    // Handle abort signal
    const abortHandler = () => {
      reader.abort();
      reject(new Error('File reading cancelled'));
    };

    if (abortSignal) {
      abortSignal.addEventListener('abort', abortHandler);
    }

    reader.onload = () => {
      if (abortSignal) {
        abortSignal.removeEventListener('abort', abortHandler);
      }
      if (typeof reader.result === 'string') {
        resolve(reader.result);
      } else {
        reject(new Error('Failed to read file'));
      }
    };

    reader.onerror = () => {
      if (abortSignal) {
        abortSignal.removeEventListener('abort', abortHandler);
      }
      reject(new Error('Failed to read file'));
    };

    reader.onabort = () => {
      if (abortSignal) {
        abortSignal.removeEventListener('abort', abortHandler);
      }
      reject(new Error('File reading cancelled'));
    };

    reader.readAsDataURL(file);
  });
}

export async function fetchImageAsBase64(url: string): Promise<string> {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      // Detect specific error types
      if (response.status === 403 || response.status === 401) {
        throw new Error('Access denied: CORS error or authentication required');
      }
      if (response.status === 404) {
        throw new Error('Image not found');
      }
      throw new Error(`Failed to fetch image: ${response.status}`);
    }

    const blob = await response.blob();
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = () => {
        if (typeof reader.result === 'string') {
          resolve(reader.result);
        } else {
          reject(new Error('Failed to read blob'));
        }
      };
      reader.onerror = () => reject(new Error('Failed to read blob'));
      reader.readAsDataURL(blob);
    });
  } catch (error) {
    // Detect CORS and network errors
    if (error instanceof TypeError && error.message.includes('Failed to fetch')) {
      throw new Error('CORS error or network failure: Could not load image');
    }
    throw error;
  }
}

export function downloadImage(base64Data: string, filename: string): void {
  const link = document.createElement('a');
  link.href = base64Data;
  link.download = filename;
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
}
