import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import { Modal } from './components/Modal';
import { AvatarUpload } from './components/AvatarUpload';
import { ProcessingView } from './components/ProcessingView';
import { ResultView } from './components/ResultView';
import { ErrorView } from './components/ErrorView';
import { QuotaBlockedView } from './components/QuotaBlockedView';
import { VirtualTryOnAPI, QuotaExhaustedError, VisitorRateLimitedError } from './api';
import {
	getAvatar,
	saveAvatar,
	clearAvatar,
	addToHistory,
	getVisitorId,
	isStorageAvailable,
	hasConsent,
} from './storage';
import { fetchImageAsBase64, compressGarmentImage, UndecodableImageError } from './imageUtils';
import { getTranslations } from './i18n';
import { trackTryOnCompleted } from './events';
import type { WidgetConfig, WidgetState, AvatarData } from './types';

interface AppProps {
	config: WidgetConfig;
}

export function App( { config }: AppProps ) {
	const t = useMemo( () => getTranslations( config.locale ), [ config.locale ] );

	const [ isModalOpen, setIsModalOpen ] = useState( false );
	const [ widgetState, setWidgetState ] = useState<WidgetState>( 'idle' );
	const [ avatar, setAvatar ] = useState<AvatarData | null>( null );
	const [ progress, setProgress ] = useState( 0 );
	const [ resultImage, setResultImage ] = useState<string | null>( null );
	const [ clothingImage, setClothingImage ] = useState<string | null>( null );
	const [ errorMessage, setErrorMessage ] = useState<string>( '' );
	const [ , setIsSubmitting ] = useState( false );
	const isSubmittingRef = useRef( false );
	const [ cooldownMinutes, setCooldownMinutes ] = useState<number | undefined>( undefined );

	// Track the current garment image; the bootstrap mutates config.productImage
	// on variation change and dispatches `aisthetix:variation-changed`.
	const productImageRef = useRef<string>( config.productImage );

	// Resolve the per-visitor rate-limit id (X-Aisthetix-Visitor). Prefer the
	// logged-in customer, then the durable first-party visitorKey (the plugin's
	// aisthetix_vid cookie — survives ITP), and finally the localStorage anon id.
	const visitorId = useMemo( () => {
		if ( config.customerId ) {
			return `customer_${ config.customerId }`;
		}
		if ( config.visitorKey ) {
			return `vid_${ config.visitorKey }`;
		}
		return getVisitorId();
	}, [ config.customerId, config.visitorKey ] );

	const api = useMemo(
		() => new VirtualTryOnAPI( config.apiBaseUrl, config.publishableKey, visitorId ),
		[ config.apiBaseUrl, config.publishableKey, visitorId ]
	);

	// Load avatar on mount.
	useEffect( () => {
		if ( isStorageAvailable() ) {
			setAvatar( getAvatar() );
		}
	}, [] );

	// Keep the garment image fresh when the shopper switches variation.
	useEffect( () => {
		const handler = ( e: Event ) => {
			const detail = ( e as CustomEvent ).detail;
			if ( detail?.productImage ) {
				productImageRef.current = detail.productImage;
			}
		};
		document.addEventListener( 'aisthetix:variation-changed', handler );
		return () => document.removeEventListener( 'aisthetix:variation-changed', handler );
	}, [] );

	const startTryOnWithAvatar = useCallback(
		async ( avatarImageData: string ) => {
			// Consent gate: this is the single funnel into generation (trigger-button
			// click on a returning visitor, retry, and the post-upload path all land
			// here), so it must be enforced here rather than only inside AvatarUpload.
			// Without this, a browser with a saved avatar from before consent existed
			// (avatar present, hasConsent() === false) could reach AI generation of a
			// person's photo without ever seeing the consent checkbox. Route back to
			// the AvatarUpload screen instead; the shopper must actively check the
			// box (which persists consent synchronously) before generation can proceed.
			if ( ! hasConsent() ) {
				setWidgetState( 'avatar-upload' );
				return;
			}

			// Prevent duplicate submissions across closures.
			if ( isSubmittingRef.current ) {
				return;
			}

			isSubmittingRef.current = true;
			setIsSubmitting( true );
			setWidgetState( 'processing' );
			setProgress( 0 );
			setErrorMessage( '' );

			try {
				// Prefer the live (possibly variation-updated) image, then
				// normalize it client-side so it's never refused for size.
				const garmentUrl = productImageRef.current || config.productImage;
				const fetchedGarment = await fetchImageAsBase64( garmentUrl );
				const clothingImageBase64 = await compressGarmentImage( fetchedGarment );
				setClothingImage( clothingImageBase64 );
				setProgress( 10 );

				// Comma-joined category slugs so the brain can route to a
				// category-aware provider (e.g. swimwear); empty when uncategorized.
				const productCategory = ( config.productCategories ?? [] ).join( ',' );

				// The brain gateway enforces quota/ledger and proxies to the engine.
				const result = await api.tryOnSync( avatarImageData, clothingImageBase64, productCategory );

				const resultImageData = `data:image/png;base64,${ result.image }`;
				setResultImage( resultImageData );
				setProgress( 100 );
				setWidgetState( 'completed' );

				// Attribution: a completed try-on is the funnel's try-on anchor
				// (try-on -> productId only). Fire-and-forget; never blocks the UI.
				trackTryOnCompleted( config, config.productId );

				addToHistory( {
					resultImage: resultImageData,
					clothingImage: clothingImageBase64,
					productTitle: config.productTitle,
					createdAt: new Date().toISOString(),
				} );
			} catch ( error ) {
				// Server-side hard stops map to the dedicated blocked views.
				if ( error instanceof QuotaExhaustedError ) {
					setWidgetState( 'quota-exceeded' );
				} else if ( error instanceof VisitorRateLimitedError ) {
					setCooldownMinutes( error.cooldownMinutesRemaining );
					setWidgetState( 'visitor-rate-limited' );
				} else {
					setErrorMessage( getErrorMessage( error ) );
					setWidgetState( 'error' );
				}
			} finally {
				isSubmittingRef.current = false;
				setIsSubmitting( false );
			}
		},
		// visitorId flows into the brain via `api` (X-Aisthetix-Visitor header).
		[ api, config, config.productImage, config.productTitle, config.productId ]
	);

	const handleAvatarSaved = useCallback(
		( imageData: string ) => {
			const newAvatar: AvatarData = {
				imageData,
				createdAt: new Date().toISOString(),
			};
			saveAvatar( imageData );
			setAvatar( newAvatar );
			startTryOnWithAvatar( imageData );
		},
		[ startTryOnWithAvatar ]
	);

	// Bind to the trigger button rendered by the plugin.
	useEffect( () => {
		const triggerButton = document.getElementById( 'aisthetix-tryon-trigger' );
		if ( ! triggerButton ) {
			return;
		}
		const handleClick = () => {
			setIsModalOpen( true );
			const currentAvatar = getAvatar();
			if ( currentAvatar ) {
				startTryOnWithAvatar( currentAvatar.imageData );
			} else {
				setWidgetState( 'avatar-upload' );
			}
		};
		triggerButton.addEventListener( 'click', handleClick );
		// Tell bootstrap.js the trigger is now wired, so it can replay a cold
		// first click instead of guessing with a fixed delay (which raced this
		// effect and silently dropped the very first click).
		window.AISTHETIX_WIDGET_READY = true;
		return () => {
			triggerButton.removeEventListener( 'click', handleClick );
			window.AISTHETIX_WIDGET_READY = false;
		};
	}, [ startTryOnWithAvatar ] );

	const handleRetry = useCallback( () => {
		if ( avatar ) {
			startTryOnWithAvatar( avatar.imageData );
		} else {
			setWidgetState( 'avatar-upload' );
		}
	}, [ avatar, startTryOnWithAvatar ] );

	const handleTryAnother = useCallback( () => {
		clearAvatar();
		setAvatar( null );
		setResultImage( null );
		setWidgetState( 'avatar-upload' );
	}, [] );

	const handleClose = useCallback( () => {
		setIsModalOpen( false );
		setTimeout( () => {
			setWidgetState( 'idle' );
			setProgress( 0 );
			setResultImage( null );
			setClothingImage( null );
			setErrorMessage( '' );
		}, 300 );
	}, [] );

	const getErrorMessage = ( error: unknown ): string => {
		if ( error instanceof UndecodableImageError ) {
			return t.errors.imageLoad;
		}
		if ( ! ( error instanceof Error ) ) {
			return t.errors.unexpected;
		}
		const message = error.message.toLowerCase();
		if ( message.includes( 'failed to fetch' ) || message.includes( 'cors' ) ) {
			return t.errors.network;
		}
		if ( message.includes( 'failed to fetch image' ) || message.includes( 'could not load image' ) ) {
			return t.errors.imageLoad;
		}
		if ( message.includes( 'timed out' ) ) {
			return t.errors.timeout;
		}
		if ( message.includes( 'api error' ) || message.includes( '401' ) || message.includes( '403' ) ) {
			return t.errors.server;
		}
		return t.errors.unexpected;
	};

	const getModalTitle = () => {
		switch ( widgetState ) {
			case 'avatar-upload':
				return t.modal.uploadPhoto;
			case 'processing':
				return t.modal.creatingTryOn;
			case 'completed':
				return t.modal.yourTryOn;
			case 'error':
				return t.modal.oops;
			case 'quota-exceeded':
			case 'visitor-rate-limited':
				return t.modal.virtualTryOn;
			default:
				return t.modal.virtualTryOn;
		}
	};

	return (
		<Modal isOpen={ isModalOpen } onClose={ handleClose } title={ getModalTitle() }>
			{ widgetState === 'quota-exceeded' && (
				<QuotaBlockedView
					status="quota_exhausted"
					planName={ null }
					onClose={ handleClose }
					t={ t.quota }
				/>
			) }

			{ widgetState === 'visitor-rate-limited' && (
				<QuotaBlockedView
					status="visitor_rate_limited"
					planName={ null }
					cooldownMinutesRemaining={ cooldownMinutes }
					onClose={ handleClose }
					t={ t.quota }
				/>
			) }

			{ widgetState === 'avatar-upload' && (
				<AvatarUpload
					onAvatarSaved={ handleAvatarSaved }
					onCancel={ handleClose }
					t={ t.upload }
					initialPreview={ avatar?.imageData ?? null }
				/>
			) }

			{ widgetState === 'processing' && (
				<ProcessingView
					progress={ progress }
					clothingImage={ clothingImage || undefined }
					t={ t.processing }
				/>
			) }

			{ widgetState === 'completed' && resultImage && (
				<ResultView
					resultImage={ resultImage }
					originalClothingImage={ clothingImage || undefined }
					productTitle={ config.productTitle }
					onTryAnother={ handleTryAnother }
					onClose={ handleClose }
					t={ t.result }
				/>
			) }

			{ widgetState === 'error' && (
				<ErrorView
					message={ errorMessage }
					onRetry={ handleRetry }
					onClose={ handleClose }
					t={ t.error }
				/>
			) }
		</Modal>
	);
}
