import type { TryOnSyncResponse } from './types';

/**
 * Error thrown when the brain hard-stops a try-on because the store's quota is
 * exhausted (HTTP 402) — the storefront button should already be hidden by the
 * plugin's cached status, but this covers the ~10-minute cache race.
 */
export class QuotaExhaustedError extends Error {
	constructor( message = 'Quota exhausted' ) {
		super( message );
		this.name = 'QuotaExhaustedError';
	}
}

/**
 * Error thrown when the visitor is rate-limited (HTTP 429).
 */
export class VisitorRateLimitedError extends Error {
	public cooldownMinutesRemaining?: number;
	constructor( message = 'Visitor rate limited', cooldownMinutesRemaining?: number ) {
		super( message );
		this.name = 'VisitorRateLimitedError';
		this.cooldownMinutesRemaining = cooldownMinutesRemaining;
	}
}

/**
 * Client for the brain storefront try-on gateway.
 *
 *   POST {apiBaseUrl}
 *   headers: X-Aisthetix-Key: <publishableKey>
 *            X-Aisthetix-Visitor: <visitorId>  (per-visitor rate-limit key)
 *   body:    multipart/form-data  user_image=<file>  clothing_image=<file>
 *            product_category=<comma-joined slugs>  (optional, provider routing)
 *   -> 200 { image: "<base64 png>", cached: bool }
 *   -> 402 quota exhausted (hard stop)
 *   -> 429 visitor rate-limited
 *
 * The browser talks DIRECTLY to the brain — never through WordPress/PHP.
 */
export class VirtualTryOnAPI {
	private endpoint: string;
	private publishableKey: string;
	private visitorId: string;

	/**
	 * @param endpoint       Full try-on endpoint, e.g. {BRAIN_URL}/api/storefront/tryon
	 * @param publishableKey Domain-restricted publishable key.
	 * @param visitorId      Stable per-visitor id; the brain keys its
	 *                       visitor_usage cooldown to this (DESIGN.md #15, §7).
	 */
	constructor( endpoint: string, publishableKey: string, visitorId: string ) {
		// Accept either the full endpoint or a base URL ending in a slash.
		this.endpoint = endpoint.replace( /\/$/, '' );
		this.publishableKey = publishableKey;
		this.visitorId = visitorId;
	}

	/**
	 * Run a synchronous try-on. Returns the result image directly (no polling).
	 *
	 * @param userImageBase64     Shopper avatar (data URL or raw base64).
	 * @param clothingImageBase64 Garment image (data URL or raw base64).
	 * @param productCategory     Comma-joined product category slugs, used by the
	 *                            brain to route to a category-aware provider (e.g.
	 *                            swimwear). Omitted from the request when empty.
	 * @param abortSignal         Optional cancellation signal.
	 */
	async tryOnSync(
		userImageBase64: string,
		clothingImageBase64: string,
		productCategory = '',
		abortSignal?: AbortSignal
	): Promise<TryOnSyncResponse> {
		const formData = new FormData();
		formData.append( 'user_image', this.base64ToBlob( userImageBase64 ), 'user.jpg' );
		formData.append( 'clothing_image', this.base64ToBlob( clothingImageBase64 ), 'clothing.jpg' );
		if ( productCategory ) {
			formData.append( 'product_category', productCategory );
		}

		const response = await fetch( this.endpoint, {
			method: 'POST',
			headers: {
				// Publishable key; Origin must match the store's registered domain.
				'X-Aisthetix-Key': this.publishableKey,
				// Per-visitor identifier the brain uses to enforce the
				// visitor_usage rate-limit cooldown (429 path).
				'X-Aisthetix-Visitor': this.visitorId,
			},
			body: formData,
			signal: abortSignal,
		} );

		if ( response.status === 402 ) {
			throw new QuotaExhaustedError();
		}

		if ( response.status === 429 ) {
			const body = await response.json().catch( () => ( {} ) );
			const cooldown =
				typeof body?.cooldownMinutesRemaining === 'number'
					? body.cooldownMinutesRemaining
					: undefined;
			throw new VisitorRateLimitedError( 'Visitor rate limited', cooldown );
		}

		if ( ! response.ok ) {
			const error = await response.json().catch( () => ( { detail: 'Unknown error' } ) );
			throw new Error( error.detail || `API error: ${ response.status }` );
		}

		return response.json();
	}

	/**
	 * Convert a base64 / data-URL string to a Blob for multipart upload.
	 */
	private base64ToBlob( base64: string ): Blob {
		const base64Data = base64.replace( /^data:image\/\w+;base64,/, '' );
		const binaryString = atob( base64Data );
		const bytes = new Uint8Array( binaryString.length );
		for ( let i = 0; i < binaryString.length; i++ ) {
			bytes[ i ] = binaryString.charCodeAt( i );
		}
		return new Blob( [ bytes ], { type: 'image/jpeg' } );
	}
}
