import { Component, useEffect, useMemo, useRef, useState } from 'react';
import type { ReactNode } from 'react';
import type { WidgetTranslations } from '../i18n';
import type { ProductVariantInput, TargetingRule, WidgetSizeConfig } from '../types';
import {
	MAX_HEIGHT_CM,
	MAX_WEIGHT_KG,
	MIN_HEIGHT_CM,
	MIN_WEIGHT_KG,
	SizeAPI,
	SizeEstimateError,
	dataUrlToBlob,
	type QcVerdict,
	type SizeEstimate,
} from '../sizeApi';
import { getBodyInputs, saveBodyInputs } from '../storage';
import { newRunId, trackProductEvent } from '../analytics';
import { fileToBase64, resizeAvatarImage } from '../imageUtils';

interface Props {
	t: WidgetTranslations;
	api: SizeAPI;
	productId: string;
	productCategories: string[];
	productVariants: ProductVariantInput[];
	sizeConfig: WidgetSizeConfig;
	photo: string;
	onAddToCart: (variantId: string, size: string) => Promise<boolean>;
	runId: string;
}

type Phase = 'offer' | 'waiting-probe' | 'inputs' | 'refining-weight' | 'estimating' | 'result' | 'failed' | 'withdrawn';

function featureEnabled(rule: TargetingRule | undefined, categories: string[], productId: string): boolean {
	if (!rule || (rule.mode !== 'include' && rule.mode !== 'exclude')) return true;
	const matched = categories.some((category) => (rule.categories || []).includes(category)) ||
		(rule.productIds || []).map(String).includes(String(productId));
	return rule.mode === 'include' ? matched : !matched;
}

function guideForProduct(size: WidgetSizeConfig, categories: string[], variants: ProductVariantInput[]) {
	if (!Array.isArray(size.guides)) return size.hasSizeGuide ? { labels: variants.map((v) => v.size_label) } : null;
	const assigned = size.guides.find((guide) =>
		(guide.categories?.categories || []).some((category) => categories.includes(category))
	);
	const fallback = size.guides.find((guide) => (guide.categories?.categories || []).length === 0);
	const guide = assigned || fallback || null;
	if (!guide) return null;
	return guide.labels.some((label) => variants.some((variant) => variant.size_label === label)) ? guide : null;
}

function qcMessage(t: WidgetTranslations, verdict: QcVerdict): string {
	if (verdict.reason === 'person_too_small') return t.size.qcPersonTooSmall;
	if (verdict.reason === 'no_person') return t.size.qcNoPerson;
	return t.size.qcNotFullBody;
}

function parseNumber(raw: string): number | null {
	if (!raw.trim()) return null;
	const value = Number(raw.trim().replace(',', '.'));
	return Number.isFinite(value) ? value : null;
}

function alternativeDirection(reason: string | undefined): 'roomier' | 'closer' | null {
	const text = (reason || '').toLowerCase();
	if (/roomier|looser|loose/.test(text)) return 'roomier';
	if (/closer|tighter|tight/.test(text)) return 'closer';
	return null;
}

function isNoMatchAnswer(estimate: SizeEstimate): boolean {
	return estimate.confidence_band === 'low' &&
		(estimate.improvement_options?.length || 0) === 0 &&
		(estimate.alternatives?.length || 0) === 0;
}

function SizeFlowInner({ t, api, productId, productCategories, productVariants, sizeConfig, photo, onAddToCart, runId }: Props) {
	const photoBlob = useMemo(() => dataUrlToBlob(photo), [photo]);
	const guide = useMemo(
		() => guideForProduct(sizeConfig, productCategories, productVariants),
		[sizeConfig, productCategories, productVariants]
	);
	const gateOpen = !!photoBlob && !!guide && featureEnabled(sizeConfig.targeting, productCategories, productId);
	const remembered = useMemo(getBodyInputs, []);
	const [qc, setQc] = useState<QcVerdict | null>(null);
	const [phase, setPhase] = useState<Phase>('offer');
	const [height, setHeight] = useState(remembered.heightCm == null ? '' : String(remembered.heightCm));
	const [weight, setWeight] = useState(remembered.weightKg == null ? '' : String(remembered.weightKg));
	const [fitPreference, setFitPreference] = useState<'tight' | 'regular' | 'loose'>(sizeConfig.defaultFitPreference || 'regular');
	const [estimate, setEstimate] = useState<SizeEstimate | null>(null);
	const [selectedVariant, setSelectedVariant] = useState<string | null>(null);
	const [error, setError] = useState('');
	const [cartError, setCartError] = useState(false);
	const [fileError, setFileError] = useState('');
	const controllerRef = useRef(new AbortController());
	const fileInputRef = useRef<HTMLInputElement>(null);
	const requestIdRef = useRef('');
	const startedAtRef = useRef(0);
	const offerTrackedRef = useRef(false);
	const qcIndexRef = useRef(0);
	const submitIndexRef = useRef(0);
	const refineIndexRef = useRef(0);
	const givenRef = useRef<{ heightCm: number | null; weightKg: number | null }>({ heightCm: null, weightKg: null });

	useEffect(() => () => controllerRef.current.abort(), []);
	useEffect(() => {
		if (!gateOpen || !photoBlob) return;
		void api.probePhotoQuality(photoBlob, controllerRef.current.signal).then((verdict) => {
			qcIndexRef.current += 1;
			const knownReasons = ['not_full_body', 'person_too_small', 'no_person'];
			trackProductEvent('size_photo_qc_completed', { ok: verdict.ok, ...(verdict.reason ? { reason: knownReasons.includes(verdict.reason) ? verdict.reason : 'unknown' } : {}), productId, runId, qcIndex: qcIndexRef.current });
			setQc(verdict);
			setPhase((current) => current === 'waiting-probe' ? (verdict.ok ? 'inputs' : 'offer') : current);
		});
	}, [api, gateOpen, photoBlob, productId, runId]);

	useEffect(() => {
		if (!gateOpen || offerTrackedRef.current) return;
		offerTrackedRef.current = true;
		trackProductEvent('size_offer_shown', {
			productId,
			guideResolution: sizeConfig.guides?.some((item) => (item.categories?.categories || []).length === 0) ? 'shop_default' : 'collection',
			runId,
		});
	}, [gateOpen, productId, runId, sizeConfig.guides]);

	if (!gateOpen || phase === 'withdrawn') return null;

	function start() {
		trackProductEvent('size_offer_accepted', { productId, runId });
		setPhase(qc === null ? 'waiting-probe' : qc.ok ? 'inputs' : 'offer');
	}

	async function submit(event: React.FormEvent) {
		event.preventDefault();
		const heightCm = phase === 'refining-weight' ? givenRef.current.heightCm : parseNumber(height);
		const weightKg = parseNumber(weight);
		if (heightCm === null || heightCm < MIN_HEIGHT_CM || heightCm > MAX_HEIGHT_CM) {
			setError(t.size.invalidHeight);
			return;
		}
		if ((phase === 'refining-weight' && weightKg === null) || (weightKg !== null && (weightKg < MIN_WEIGHT_KG || weightKg > MAX_WEIGHT_KG))) {
			setError(t.size.invalidWeight);
			return;
		}
		if (!photoBlob) return;
		setError('');
		submitIndexRef.current += 1;
		trackProductEvent('size_inputs_submitted', { hasWeight: weightKg !== null, productId, runId, submitIndex: submitIndexRef.current });
		saveBodyInputs(heightCm, weightKg);
		givenRef.current = { heightCm, weightKg };
		setPhase('estimating');
		requestIdRef.current = newRunId();
		startedAtRef.current = Date.now();
		trackProductEvent('size_estimation_requested', { requestId: requestIdRef.current, hasWeight: weightKg !== null, productId, runId });
		try {
			const result = await api.estimate({
				productId,
				productCategories,
				productVariants,
				heightCm,
				weightKg,
				fitPreference,
				photo: photoBlob,
			}, controllerRef.current.signal);
			setEstimate(result);
			setSelectedVariant(result.recommended_variant_id || null);
			trackProductEvent('size_estimation_completed', {
				requestId: requestIdRef.current,
				recommendedSize: result.recommended_size,
				confidenceBand: result.confidence_band,
				cacheHit: result.cache_hit === true,
				betweenSizes: result.between_sizes === true,
				durationMs: Date.now() - startedAtRef.current,
				productId,
				runId,
			});
			setPhase('result');
		} catch (caught) {
			const rawReason = caught instanceof SizeEstimateError ? caught.code : 'unexpected';
			const declaredReasons = ['photo_unusable', 'size_guide_not_usable', 'size_guide_not_applicable', 'engine_unavailable', 'timeout', 'invalid_input', 'unreachable', 'unreadable_response', 'unexpected'];
			const reason = declaredReasons.includes(rawReason) ? rawReason : rawReason === 'invalid_height' || rawReason === 'invalid_weight' ? 'invalid_input' : 'unexpected';
			trackProductEvent('size_estimation_failed', {
				requestId: requestIdRef.current,
				reason,
				retryable: reason === 'engine_unavailable' || reason === 'timeout' || reason === 'unreachable',
				durationMs: Date.now() - startedAtRef.current,
				productId,
				runId,
			});
			if (caught instanceof SizeEstimateError && (caught.code === 'size_guide_not_usable' || caught.code === 'size_guide_not_applicable')) {
				setPhase('withdrawn');
				return;
			}
			if (caught instanceof SizeEstimateError && caught.code === 'photo_unusable') {
				setQc({ ok: false, reason: caught.reason });
				setPhase('offer');
				return;
			}
			setPhase('failed');
		}
	}

	function refineWithWeight() {
		refineIndexRef.current += 1;
		trackProductEvent('size_refinement_requested', { kind: 'weight', productId, runId, refineIndex: refineIndexRef.current });
		setWeight('');
		setError('');
		setPhase('refining-weight');
	}

	async function refineWithPhoto(file: File) {
		if (!['image/jpeg', 'image/png', 'image/webp'].includes(file.type)) {
			setFileError(t.size.invalidPhoto);
			return;
		}
		setFileError('');
		refineIndexRef.current += 1;
		trackProductEvent('size_refinement_requested', { kind: 'photo', productId, runId, refineIndex: refineIndexRef.current });
		setPhase('estimating');
		try {
			const resized = await resizeAvatarImage(await fileToBase64(file));
			const replacement = dataUrlToBlob(resized);
			const { heightCm, weightKg } = givenRef.current;
			if (!replacement || heightCm === null) throw new Error('invalid replacement photo');
			requestIdRef.current = newRunId();
			startedAtRef.current = Date.now();
			trackProductEvent('size_estimation_requested', { requestId: requestIdRef.current, hasWeight: weightKg !== null, productId, runId });
			const result = await api.estimate({ productId, productCategories, productVariants, heightCm, weightKg, fitPreference, photo: replacement }, controllerRef.current.signal);
			setEstimate(result);
			setSelectedVariant(result.recommended_variant_id || null);
			trackProductEvent('size_estimation_completed', {
				requestId: requestIdRef.current,
				recommendedSize: result.recommended_size,
				confidenceBand: result.confidence_band,
				cacheHit: result.cache_hit === true,
				betweenSizes: result.between_sizes === true,
				durationMs: Date.now() - startedAtRef.current,
				productId,
				runId,
			});
			setPhase('result');
		} catch {
			trackProductEvent('size_estimation_failed', {
				requestId: requestIdRef.current,
				reason: 'unexpected',
				retryable: false,
				durationMs: startedAtRef.current ? Date.now() - startedAtRef.current : 0,
				productId,
				runId,
			});
			setPhase('failed');
		}
	}

	const alternative = estimate?.between_sizes ? estimate.alternatives?.[0] : undefined;
	const choices = estimate ? [
		{ size: estimate.recommended_size, variantId: estimate.recommended_variant_id },
		...(alternative ? [{ size: alternative.size, variantId: alternative.variant_id }] : []),
	].filter((choice) => !!choice.variantId) : [];
	const selected = choices.find((choice) => choice.variantId === selectedVariant);
	const direction = alternativeDirection(alternative?.reason);
	const bandCopy = estimate && isNoMatchAnswer(estimate)
		? t.size.bandNoMatch
		: estimate?.confidence_band === 'very_confident'
			? t.size.bandVeryConfident
			: estimate?.confidence_band === 'good'
				? t.size.bandGood
				: t.size.bandLow;

	return (
		<section className="aisthetix-size">
			{phase === 'offer' && (
				<div className={`aisthetix-size-offer${qc && !qc.ok ? ' aisthetix-size-offer--blocked' : ''}`}>
					<p className="aisthetix-size-offer__title">{t.size.offerTitle}</p>
					{qc && !qc.ok ? <p role="alert">{qcMessage(t, qc)}</p> : (
						<button type="button" className="aisthetix-btn aisthetix-btn-primary" onClick={start}>{t.size.offerCta}</button>
					)}
				</div>
			)}
			{phase === 'waiting-probe' && <p role="status">{t.size.checkingPhoto}</p>}
			{(phase === 'inputs' || phase === 'refining-weight') && (
				<form className="aisthetix-size-form" onSubmit={(event) => void submit(event)} noValidate>
					{phase !== 'refining-weight' && <label>{t.size.heightLabel}<input type="number" min={MIN_HEIGHT_CM} max={MAX_HEIGHT_CM} inputMode="numeric" value={height} onChange={(event) => setHeight(event.target.value)} /></label>}
					<label>{t.size.weightLabel}<input type="number" min={MIN_WEIGHT_KG} max={MAX_WEIGHT_KG} inputMode="numeric" value={weight} onChange={(event) => setWeight(event.target.value)} /></label>
					{phase !== 'refining-weight' && sizeConfig.fitPreferenceEnabled && <label>{t.size.fitLabel}<select value={fitPreference} onChange={(event) => setFitPreference(event.target.value as 'tight' | 'regular' | 'loose')}><option value="tight">{t.size.fitTight}</option><option value="regular">{t.size.fitRegular}</option><option value="loose">{t.size.fitLoose}</option></select></label>}
					{error && <p role="alert">{error}</p>}
					<button className="aisthetix-btn aisthetix-btn-primary" type="submit">{t.size.submit}</button>
				</form>
			)}
			{phase === 'estimating' && <p role="status">{t.size.estimating}</p>}
			{phase === 'result' && estimate && (
				<div className="aisthetix-size-result">
					<p className="aisthetix-size-result__title">{t.size.resultTitle}</p>
					<p className="aisthetix-size-result__size">{estimate.recommended_size}</p>
					<p>{bandCopy}</p>
					{estimate.between_sizes && <p>{t.size.betweenSizes}</p>}
					{alternative && <p>{direction === 'roomier' ? t.size.alsoConsiderRoomier(alternative.size) : direction === 'closer' ? t.size.alsoConsiderCloser(alternative.size) : t.size.alsoConsider(alternative.size)}</p>}
					{choices.length > 1 && <div className="aisthetix-size-result__choices">{choices.map((choice) => (
						<button type="button" key={choice.variantId} aria-pressed={selectedVariant === choice.variantId} onClick={() => {
							setSelectedVariant(choice.variantId);
							trackProductEvent('size_recommended_variant_selected', { recommendedSize: choice.size, variantId: choice.variantId, productId, runId, shopperOverrode: choice.variantId !== estimate.recommended_variant_id });
						}}>{choice.size}</button>
					))}</div>}
					{selected && <p>{t.size.cartUsesSize(selected.size)}</p>}
					{selected && <button type="button" className="aisthetix-btn aisthetix-btn-primary" onClick={() => {
						trackProductEvent('size_recommended_variant_added_to_cart', { recommendedSize: selected.size, variantId: selected.variantId, productId, runId, shopperOverrode: selected.variantId !== estimate.recommended_variant_id });
						void onAddToCart(selected.variantId, selected.size).then((ok) => setCartError(!ok));
					}}>{t.size.addToCart}</button>}
					{cartError && <p role="alert">{t.size.addToCartError}</p>}
					{estimate.improvement_options?.includes('add_weight') && givenRef.current.weightKg === null && <button type="button" className="aisthetix-btn aisthetix-btn-secondary" onClick={refineWithWeight}>{t.size.addWeight}</button>}
					{estimate.improvement_options?.includes('retake_photo') && <>
						<label className="aisthetix-btn aisthetix-btn-secondary" htmlFor="aisthetix-size-photo">{t.size.retakePhoto}</label>
						<input ref={fileInputRef} id="aisthetix-size-photo" className="aisthetix-size-result__file" type="file" accept="image/jpeg,image/png,image/webp" onChange={(event) => {
							const file = event.target.files?.[0];
							if (fileInputRef.current) fileInputRef.current.value = '';
							if (file) void refineWithPhoto(file);
						}} />
					</>}
					{fileError && <p role="alert">{fileError}</p>}
				</div>
			)}
			{phase === 'failed' && <p role="alert">{t.size.failed}</p>}
		</section>
	);
}

class SizeBoundary extends Component<{ children: ReactNode }, { failed: boolean }> {
	state = { failed: false };
	static getDerivedStateFromError() { return { failed: true }; }
	render() { return this.state.failed ? null : this.props.children; }
}

export function SizeFlow(props: Props) {
	return <SizeBoundary><SizeFlowInner {...props} /></SizeBoundary>;
}
