/**
 * Wizard step 3: kick off matching, then review/confirm/reject/assign rows.
 */

import { useCallback, useEffect, useState } from '@wordpress/element';
import { Button, Notice, SelectControl, Spinner } from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n';

import {
	adjudicateJob,
	assignRow,
	confirmRow,
	getJobProgress,
	getJobRows,
	getLlmEstimate,
	matchJob,
	rejectRow,
} from '../api';
import ProductAssignPicker from './ProductAssignPicker';

const STATUS_FILTERS = [
	{ value: '', label: __( 'All', 'emberprice' ) },
	{ value: 'auto', label: __( 'Auto', 'emberprice' ) },
	{ value: 'review', label: __( 'Needs review', 'emberprice' ) },
	{ value: 'confirmed', label: __( 'Confirmed', 'emberprice' ) },
	{ value: 'rejected', label: __( 'Rejected', 'emberprice' ) },
	{ value: 'skipped', label: __( 'Skipped', 'emberprice' ) },
];

const STAGE_LABELS = {
	alias: __( 'Alias', 'emberprice' ),
	sku: __( 'SKU', 'emberprice' ),
	exact_name: __( 'Exact name', 'emberprice' ),
	fuzzy: __( 'Fuzzy', 'emberprice' ),
	llm: __( 'AI', 'emberprice' ),
	manual: __( 'Manual', 'emberprice' ),
	none: __( 'None', 'emberprice' ),
};

const EMPTY_ROWS_PAGE = { rows: [], total: 0, per_page: 50, counts: {} };

export default function ReviewStep( { job, onReviewed } ) {
	const [ progress, setProgress ] = useState( null );
	const [ statusFilter, setStatusFilter ] = useState( '' );
	const [ page, setPage ] = useState( 1 );
	const [ rowsData, setRowsData ] = useState( EMPTY_ROWS_PAGE );
	const [ isLoadingRows, setIsLoadingRows ] = useState( false );
	const [ focusedRowId, setFocusedRowId ] = useState( null );
	const [ assigningRowId, setAssigningRowId ] = useState( null );
	const [ error, setError ] = useState( '' );
	const [ llmEstimate, setLlmEstimate ] = useState( null );
	const [ isAdjudicating, setIsAdjudicating ] = useState( false );
	const [ retryToken, setRetryToken ] = useState( 0 );

	const matchingFailed =
		'failed' === progress?.stage && 'matching' === progress?.error?.stage;
	const isMatching =
		! progress || 'matching' === progress.stage || matchingFailed;

	// Kick off matching once on mount, then poll progress until it's done.
	// A 409 here just means matching was already started (or finished) by
	// an earlier visit to this step -- not an error, so it's swallowed and
	// polling starts anyway.
	useEffect( () => {
		let cancelled = false;
		let intervalId = null;

		const poll = () => {
			getJobProgress( job.id )
				.then( ( result ) => {
					if ( cancelled ) {
						return;
					}
					setProgress( result );
					if (
						( 'matching' !== result.stage || result.stalled ) &&
						intervalId
					) {
						clearInterval( intervalId );
					}
				} )
				.catch( ( progressError ) => {
					if ( ! cancelled ) {
						setError(
							progressError?.message ||
								__(
									'Could not read matching progress.',
									'emberprice'
								)
						);
					}
				} );
		};

		matchJob( job.id, retryToken > 0 )
			.catch( ( matchError ) => {
				if ( 'embp_invalid_job_status' !== matchError?.code ) {
					setError(
						matchError?.message ||
							__( 'Could not start matching.', 'emberprice' )
					);
				}
			} )
			.finally( () => {
				if ( cancelled ) {
					return;
				}
				poll();
				intervalId = setInterval( poll, 1500 );
			} );

		return () => {
			cancelled = true;
			if ( intervalId ) {
				clearInterval( intervalId );
			}
		};
	}, [ job.id, retryToken ] );

	const handleRetryMatching = () => {
		setError( '' );
		setProgress( null );
		setRetryToken( ( current ) => current + 1 );
	};

	const loadRows = useCallback( () => {
		setIsLoadingRows( true );
		getJobRows( job.id, { status: statusFilter, page } )
			.then( ( result ) => {
				setIsLoadingRows( false );
				setRowsData( result );
				setFocusedRowId( ( current ) =>
					result.rows.some( ( row ) => row.id === current )
						? current
						: result.rows[ 0 ]?.id ?? null
				);
			} )
			.catch( ( loadError ) => {
				setIsLoadingRows( false );
				setError(
					loadError?.message ||
						__( 'Could not load rows.', 'emberprice' )
				);
			} );
	}, [ job.id, statusFilter, page ] );

	useEffect( () => {
		if ( ! isMatching ) {
			loadRows();
		}
	}, [ isMatching, loadRows ] );

	const refreshLlmEstimate = useCallback( () => {
		getLlmEstimate( job.id )
			.then( setLlmEstimate )
			.catch( () => setLlmEstimate( { available: false } ) );
	}, [ job.id ] );

	useEffect( () => {
		if ( ! isMatching ) {
			refreshLlmEstimate();
		}
	}, [ isMatching, refreshLlmEstimate ] );

	const handleAdjudicate = () => {
		setIsAdjudicating( true );

		adjudicateJob( job.id )
			.then( () => pollAdjudication() )
			.catch( ( adjudicateError ) => {
				setIsAdjudicating( false );
				setError(
					adjudicateError?.message ||
						__( 'Could not run AI matching.', 'emberprice' )
				);
			} );
	};

	const pollAdjudication = () => {
		getJobProgress( job.id ).then( ( result ) => {
			if ( 'llm_adjudicated' === result.stage ) {
				setIsAdjudicating( false );
				loadRows();
				refreshLlmEstimate();
				return;
			}

			setTimeout( pollAdjudication, 1500 );
		} );
	};

	const applyRowUpdate = ( updatedRow ) => {
		setRowsData( ( previous ) => ( {
			...previous,
			rows: previous.rows.map( ( row ) =>
				row.id === updatedRow.id ? updatedRow : row
			),
		} ) );
	};

	const focusNextRow = ( currentId ) => {
		const index = rowsData.rows.findIndex(
			( row ) => row.id === currentId
		);
		const next = rowsData.rows[ index + 1 ];
		setFocusedRowId( next ? next.id : null );
	};

	const handleConfirm = ( rowId ) => {
		confirmRow( rowId )
			.then( ( result ) => {
				applyRowUpdate( result.row );
				focusNextRow( rowId );
			} )
			.catch( ( confirmError ) =>
				setError(
					confirmError?.message ||
						__( 'Could not confirm this row.', 'emberprice' )
				)
			);
	};

	const handleReject = ( rowId ) => {
		rejectRow( rowId )
			.then( ( result ) => {
				applyRowUpdate( result.row );
				focusNextRow( rowId );
			} )
			.catch( ( rejectError ) =>
				setError(
					rejectError?.message ||
						__( 'Could not reject this row.', 'emberprice' )
				)
			);
	};

	const handleAssign = ( rowId, productId, variationId ) => {
		assignRow( rowId, productId, variationId )
			.then( ( result ) => {
				applyRowUpdate( result.row );
				setAssigningRowId( null );
				focusNextRow( rowId );
			} )
			.catch( ( assignError ) =>
				setError(
					assignError?.message ||
						__( 'Could not assign this product.', 'emberprice' )
				)
			);
	};

	// Keyboard shortcuts: Enter confirms, X rejects, S skips to the next
	// row -- disabled while typing in a field or while the assign picker
	// is open, and re-registered whenever the focused row or row list
	// changes so the handler always acts on current data.
	useEffect( () => {
		if ( isMatching ) {
			return undefined;
		}

		const handleKeyDown = ( event ) => {
			const tag = event.target?.ownerDocument?.activeElement?.tagName;

			if (
				'INPUT' === tag ||
				'TEXTAREA' === tag ||
				'SELECT' === tag ||
				null === focusedRowId ||
				assigningRowId
			) {
				return;
			}

			if ( 'Enter' === event.key ) {
				event.preventDefault();
				handleConfirm( focusedRowId );
			} else if ( 'x' === event.key || 'X' === event.key ) {
				event.preventDefault();
				handleReject( focusedRowId );
			} else if ( 's' === event.key || 'S' === event.key ) {
				event.preventDefault();
				focusNextRow( focusedRowId );
			}
		};

		window.addEventListener( 'keydown', handleKeyDown );

		return () => window.removeEventListener( 'keydown', handleKeyDown );
		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, [ isMatching, focusedRowId, assigningRowId, rowsData.rows ] );

	if ( isMatching ) {
		const done = progress?.done ?? 0;
		const total = progress?.total ?? 0;
		const percent = total > 0 ? Math.round( ( done / total ) * 100 ) : 0;

		return (
			<div className="embp-review-step">
				{ ( progress?.stalled || matchingFailed ) && (
					<Notice status="error" isDismissible={ false }>
						<p>
							{ progress?.message ||
								progress?.error?.message ||
								__(
									'Matching stopped before it completed.',
									'emberprice'
								) }
						</p>
						<Button
							variant="secondary"
							onClick={ handleRetryMatching }
						>
							{ __( 'Retry matching', 'emberprice' ) }
						</Button>
					</Notice>
				) }
				<p>
					{ __(
						'Matching rows against your catalog…',
						'emberprice'
					) }
				</p>
				<div
					className="embp-progress"
					role="progressbar"
					aria-valuemin="0"
					aria-valuemax={ total }
					aria-valuenow={ done }
					aria-label={ __( 'Matching progress', 'emberprice' ) }
				>
					<div
						className="embp-progress__bar"
						style={ { width: `${ percent }%` } }
					/>
					<span className="embp-progress__label">
						{ sprintf(
							/* translators: 1: rows matched so far, 2: total rows */
							__( '%1$d / %2$d', 'emberprice' ),
							done,
							total
						) }
					</span>
				</div>
			</div>
		);
	}

	const totalPages = Math.max(
		1,
		Math.ceil( rowsData.total / rowsData.per_page )
	);
	let rowsContent = null;

	if ( isLoadingRows ) {
		rowsContent = <Spinner />;
	} else if ( 0 === rowsData.rows.length ) {
		rowsContent = (
			<p>{ __( 'No rows match this filter.', 'emberprice' ) }</p>
		);
	} else {
		rowsContent = (
			<div className="embp-review-table-wrap">
				<table className="embp-review-table">
					<thead>
						<tr>
							<th scope="col">
								{ __( 'Supplier row', 'emberprice' ) }
							</th>
							<th scope="col">
								{ __( 'Matched product', 'emberprice' ) }
							</th>
							<th scope="col">{ __( 'Stage', 'emberprice' ) }</th>
							<th scope="col">
								{ __( 'Confidence', 'emberprice' ) }
							</th>
							<th scope="col">{ __( 'Flags', 'emberprice' ) }</th>
							<th scope="col">
								{ __( 'Status', 'emberprice' ) }
							</th>
							<th scope="col">
								{ __( 'Actions', 'emberprice' ) }
							</th>
						</tr>
					</thead>
					<tbody>
						{ rowsData.rows.map( ( row ) => (
							<tr
								key={ row.id }
								className={
									row.id === focusedRowId
										? 'embp-review-row is-focused'
										: 'embp-review-row'
								}
								onClick={ () => setFocusedRowId( row.id ) }
								onFocus={ () => setFocusedRowId( row.id ) }
								tabIndex="0"
							>
								<td>
									{ row.parsed?.name }
									{ row.parsed?.code
										? ` (${ row.parsed.code })`
										: '' }
								</td>
								<td>
									{ row.matched_product
										? `${ row.matched_product.name } (${ row.matched_product.sku })`
										: '—' }
								</td>
								<td>
									{ row.match_stage
										? STAGE_LABELS[ row.match_stage ] ??
										  row.match_stage
										: '—' }
								</td>
								<td>
									{ null !== row.confidence
										? row.confidence.toFixed( 2 )
										: '—' }
								</td>
								<td>
									{ row.flags && row.flags.length > 0
										? row.flags.join( ', ' )
										: '—' }
								</td>
								<td>{ row.status }</td>
								<td className="embp-review-row__actions">
									<Button
										variant="secondary"
										size="small"
										disabled={ ! row.match_product_id }
										onClick={ () =>
											handleConfirm( row.id )
										}
									>
										{ __( 'Confirm', 'emberprice' ) }
									</Button>
									<Button
										variant="secondary"
										size="small"
										onClick={ () => handleReject( row.id ) }
									>
										{ __( 'Reject', 'emberprice' ) }
									</Button>
									<Button
										variant="tertiary"
										size="small"
										onClick={ () =>
											setAssigningRowId(
												assigningRowId === row.id
													? null
													: row.id
											)
										}
									>
										{ __( 'Assign…', 'emberprice' ) }
									</Button>
									{ assigningRowId === row.id && (
										<ProductAssignPicker
											onSelect={ (
												productId,
												variationId
											) =>
												handleAssign(
													row.id,
													productId,
													variationId
												)
											}
											onCancel={ () =>
												setAssigningRowId( null )
											}
										/>
									) }
								</td>
							</tr>
						) ) }
					</tbody>
				</table>
			</div>
		);
	}

	return (
		<div className="embp-review-step">
			{ error && (
				<Notice
					status="error"
					isDismissible
					onRemove={ () => setError( '' ) }
				>
					{ error }
				</Notice>
			) }

			{ llmEstimate?.available && llmEstimate.rows > 0 && (
				<div className="embp-review-llm">
					{ isAdjudicating ? (
						<>
							<Spinner />
							<span>
								{ __( 'Running AI matching…', 'emberprice' ) }
							</span>
						</>
					) : (
						<>
							<span>
								{ sprintf(
									/* translators: 1: number of rows, 2: estimated tokens */
									__(
										'AI matching available for %1$d row(s), ~%2$d estimated tokens.',
										'emberprice'
									),
									llmEstimate.rows,
									llmEstimate.estimated_tokens
								) }
							</span>
							<Button
								variant="secondary"
								onClick={ handleAdjudicate }
							>
								{ __( 'Run AI matching', 'emberprice' ) }
							</Button>
						</>
					) }
				</div>
			) }

			<div className="embp-review-counters">
				{ Object.entries( rowsData.counts ).map(
					( [ status, count ] ) => (
						<span key={ status } className="embp-review-counter">
							{ status }: { count }
						</span>
					)
				) }
			</div>

			<SelectControl
				label={ __( 'Filter by status', 'emberprice' ) }
				value={ statusFilter }
				options={ STATUS_FILTERS }
				onChange={ ( value ) => {
					setStatusFilter( value );
					setPage( 1 );
				} }
				__next40pxDefaultSize
				__nextHasNoMarginBottom
			/>

			{ rowsContent }

			<div className="embp-review-pagination">
				<Button
					variant="secondary"
					disabled={ page <= 1 }
					onClick={ () => setPage( ( current ) => current - 1 ) }
				>
					{ __( 'Previous', 'emberprice' ) }
				</Button>
				<span>
					{ sprintf(
						/* translators: 1: current page, 2: total pages */
						__( 'Page %1$d of %2$d', 'emberprice' ),
						page,
						totalPages
					) }
				</span>
				<Button
					variant="secondary"
					disabled={ page >= totalPages }
					onClick={ () => setPage( ( current ) => current + 1 ) }
				>
					{ __( 'Next', 'emberprice' ) }
				</Button>
			</div>

			<p className="embp-review-shortcuts">
				{ __(
					'Shortcuts: Enter to confirm, X to reject, S to skip to the next row.',
					'emberprice'
				) }
			</p>

			<Button variant="primary" onClick={ () => onReviewed( job ) }>
				{ __( 'Continue', 'emberprice' ) }
			</Button>
		</div>
	);
}
