/**
 * Batch Detail Drawer — Alpine store + row-click intercept.
 *
 * Mirrors the payout drawer pattern: a single Alpine store backs the drawer
 * chrome, list-row clicks are intercepted to open the drawer instead of
 * navigating, and the URL (`?batch_id=X`) is the source of truth so
 * deep-links / back-forward / sharing all work.
 *
 * When the admin clicks a payout inside the batch drawer, the batch drawer
 * closes and the existing payout drawer opens. Single-drawer invariant —
 * no drawer-over-drawer stacking in v1. The URL retains `batch_id=X` so the
 * batch drawer reopens when the admin closes the payout drawer.
 *
 * Expects `affwpBatchDrawerData` via wp_localize_script:
 *   - nonce   — AJAX nonce for `affwp_batch_drawer`
 *   - ajaxUrl — admin-ajax.php URL
 *   - listUrl — batches list admin URL
 *
 * @since 2.33.0
 */
( function () {
	'use strict';

	const DATA_KEY = 'affwpBatchDrawerData';
	const data     = window[ DATA_KEY ] || {};
	const labels   = data.labels || {};

	if ( ! data.ajaxUrl ) {
		return;
	}

	/**
	 * Substitute a single sprintf-style token (%d / %s) in a localized string.
	 *
	 * @param {string} template Localized template (may contain `%d` or `%s`).
	 * @param {*}      value    Replacement value (coerced to string).
	 * @return {string}
	 */
	function fmt( template, value ) {
		return String( template || '' ).replace( /%[ds]/, String( value ) );
	}

	const registerStore = function () {

		Alpine.store( 'batchDrawer', {

			// Drawer shell state (see trait-drawers.php contract).
			isOpen: false,
			isNew: false,
			isLoading: false,
			errors: {},
			title: '',
			subtitle: '',

			// Batch state.
			batchId: 0,
			batch: null,
			payouts: [],

			/*
			 * Status filter tab state — 'all' | 'paid' | 'processing' | 'failed'.
			 * Default 'all' on every open; not persisted per-batch because the
			 * right default changes with the batch's shape (a batch with 3
			 * failures wants 'failed' tab prominent on open, but a batch with
			 * 0 failures shouldn't open with an empty 'failed' view).
			 */
			statusFilter: 'all',

			// Action state.
			isRetrying: false,
			isNavigating: false,

			// Ordered list of batch IDs in the visible list for keyboard nav.
			batchIds: [],

			/*
			 * Live-refresh timer for batches still processing. `_pollTimer` is
			 * the setTimeout id so we can cancel on close/navigate.
			 */
			_pollTimer: null,

			// Element that had focus when the drawer was opened — restored on close.
			_returnFocusTo: null,

			/*
			 * Suppress the prev/next pager chevrons in the drawer header on the
			 * very first arrival after a Pay-Affiliates submit. The user is here
			 * because they just paid — inviting them to arrow-navigate to a
			 * different batch in that moment is a misclick magnet. The flag is
			 * set in init() when ?affwp_notice=batch_submitted is present and
			 * cleared after 4s, giving the toast (~3.5s) time to settle and the
			 * user a beat to register what just happened before the navigation
			 * affordance returns.
			 */
			_suppressPagerOnSubmit: false,

			/*
			 * ─────────────────────────────────────────────────────────────
			 * Derived getters.
			 * ─────────────────────────────────────────────────────────────
			 */

			get status() {
				return this.batch ? String( this.batch.status || '' ).toLowerCase() : '';
			},

			get failedCount() {
				return this.batch ? parseInt( this.batch.failed_count || 0, 10 ) : 0;
			},

			get canRetry() {
				return this.failedCount > 0 && ! this.isRetrying;
			},

			get listIndex() {
				return this.batchIds.indexOf( this.batchId );
			},

			get listPosition() {
				return this.listIndex >= 0 ? this.listIndex + 1 : 0;
			},

			get listTotal() {
				return this.batchIds.length;
			},

			get hasPrev() {
				return this.listIndex > 0;
			},

			get hasNext() {
				return this.listIndex >= 0 && this.listIndex < this.batchIds.length - 1;
			},

			get showPager() {
				return this.listTotal > 1 && this.listIndex >= 0 && ! this._suppressPagerOnSubmit;
			},

			/*
			 * Filter-tab options, computed from the actual payouts in the
			 * batch. Always includes 'All'; only includes a status tab when
			 * at least one payout has that status. Counts render next to
			 * each label so the tab row doubles as a summary.
			 */
			get statusFilterOptions() {
				const counts = { paid: 0, processing: 0, failed: 0 };
				( this.payouts || [] ).forEach( ( p ) => {
					const s = String( p.status || '' ).toLowerCase();
					if ( counts.hasOwnProperty( s ) ) {
						counts[ s ]++;
					}
				} );

				/*
				 * Only render tabs if the batch actually has >1 status
				 * present. A uniform batch doesn't need filter UI.
				 */
				const presentStatuses = Object.keys( counts ).filter( ( k ) => counts[ k ] > 0 );
				if ( presentStatuses.length < 2 ) {
					return [ { key: 'all', label: labels.tabAll || '', count: this.payouts.length } ];
				}

				const options = [ { key: 'all', label: labels.tabAll || '', count: this.payouts.length } ];
				if ( counts.paid > 0 )       options.push( { key: 'paid',       label: labels.tabPaid || '',    count: counts.paid       } );
				if ( counts.processing > 0 ) options.push( { key: 'processing', label: labels.tabPending || '', count: counts.processing } );
				if ( counts.failed > 0 )     options.push( { key: 'failed',     label: labels.tabFailed || '',  count: counts.failed     } );
				return options;
			},

			/*
			 * Filtered view of the payouts list driven by the active tab.
			 * 'all' short-circuits to avoid a copy on the common case.
			 */
			get filteredPayouts() {
				if ( 'all' === this.statusFilter ) {
					return this.payouts || [];
				}
				const want = this.statusFilter;
				return ( this.payouts || [] ).filter( ( p ) => String( p.status || '' ).toLowerCase() === want );
			},

			/*
			 * ─────────────────────────────────────────────────────────────
			 * Lifecycle.
			 * ─────────────────────────────────────────────────────────────
			 */

			init() {
				this._scanList();

				document.addEventListener( 'keydown', ( event ) => this._handleKeyNav( event ) );

				/*
				 * Pause polling while the tab is backgrounded — no point hammering
				 * admin-ajax every 3s when the user can't see the result. Resume
				 * immediately on visibility return so the drawer doesn't stay stale.
				 */
				document.addEventListener( 'visibilitychange', () => {
					if ( ! this.isOpen || 'processing' !== this.status ) return;
					if ( document.hidden ) {
						this._stopPolling();
					} else {
						this._refreshBatch().then( () => {
							if ( this.isOpen && 'processing' === this.status ) {
								this._schedulePoll();
							}
						} );
					}
				} );

				const params     = new URLSearchParams( window.location.search );
				const deepLinkId = parseInt( params.get( 'batch_id' ), 10 );
				const tab        = params.get( 'tab' );

				/*
				 * Post-submit arrival: hide the pager chevrons briefly so the
				 * user doesn't misclick into a different batch in the moment
				 * they're getting the "you just submitted" toast. Also clear
				 * the Pay Affiliates wizard's saved filters so the next visit
				 * to New Payout starts fresh — those filters are scratch state
				 * for the in-flight wizard, not a saved workflow.
				 */
				if ( 'batch_submitted' === params.get( 'affwp_notice' ) ) {
					try { sessionStorage.removeItem( 'affwp_pay_affiliates_filters_v1' ); } catch ( e ) {}
					this._suppressPagerOnSubmit = true;
					setTimeout( () => { this._suppressPagerOnSubmit = false; }, 4000 );
				}

				/*
				 * Only auto-open on the batches tab — avoids grabbing
				 * `?batch_id=` if something else in the admin happens to use it.
				 */
				if ( 'batches' === tab && deepLinkId > 0 ) {
					this.openFor( deepLinkId );
				}
			},

			_scanList() {
				const ids  = [];
				const seen = Object.create( null );

				/*
				 * Batch rows link to `?tab=batches&batch_id=N`. Match that
				 * href pattern, skip any anchor carrying an `affwp_action=` query
				 * arg (delete / retry-as-anchor links — keyboard nav should not
				 * include destructive URLs even if a future template adds them),
				 * and dedupe to preserve display order. Mirrors the same
				 * affwp_action guard used by the row-click intercept below.
				 */
				document.querySelectorAll( 'a[href*="tab=batches"][href*="batch_id="]' ).forEach( ( link ) => {
					try {
						const url = new URL( link.href, window.location.origin );
						if ( url.searchParams.get( 'affwp_action' ) ) {
							return;
						}
						const id  = parseInt( url.searchParams.get( 'batch_id' ), 10 );
						if ( id && ! seen[ id ] ) {
							seen[ id ] = true;
							ids.push( id );
						}
					} catch ( e ) {
						// Skip malformed hrefs.
					}
				} );

				this.batchIds = ids;
			},

			async openFor( batchId ) {

				const nextId = parseInt( batchId, 10 );
				const isNav  = !! this.batch && nextId !== this.batchId;

				// Save the focused element on first-open so close() can restore it.
				if ( ! isNav && ! this._returnFocusTo ) {
					const active = window.document.activeElement;
					this._returnFocusTo = ( active && 'function' === typeof active.focus ) ? active : null;
				}

				this.batchId      = nextId;
				this.isNavigating = isNav;
				this.isLoading    = true;
				this.isOpen       = true;
				this.errors       = {};
				this.title        = fmt( labels.batchTitle, this.batchId );
				this.subtitle     = '';

				/*
				 * Lifecycle event for third-party listeners. See payout
				 * drawer for the contract.
				 */
				window.dispatchEvent( new CustomEvent( 'affwp-drawer-opened', {
					detail: { drawer: 'batch', id: this.batchId, isNavigating: isNav }
				} ) );

				this._syncUrl();
				this._stopPolling();

				try {
					await this._refreshBatch();
				} finally {
					this.isLoading    = false;
					this.isNavigating = false;
				}

				/*
				 * If the batch is still draining via AS, start polling so
				 * progress updates appear live. Stops automatically when the
				 * status transitions to a terminal value.
				 */
				if ( 'processing' === this.status ) {
					this._schedulePoll();
				}
			},

			async _refreshBatch() {
				// Capture status before the fetch to detect a terminal transition once new data lands and fire a completion toast.
				const prevStatus = this.batch ? String( this.batch.status || '' ).toLowerCase() : '';

				try {
					const body = new URLSearchParams();
					body.append( 'action', 'affwp_get_batch_drawer_data' );
					body.append( 'batch_id', this.batchId );
					body.append( '_ajax_nonce', data.nonces.affwp_get_batch_drawer_data );

					const res  = await fetch( data.ajaxUrl, { method: 'POST', credentials: 'same-origin', body: body } );
					const json = await res.json();

					if ( ! json || ! json.success ) {
						this.errors.general = ( json && json.data && json.data.message ) || labels.loadFailed || '';
						this.batch   = null;
						this.payouts = [];
						return;
					}

					this.batch   = json.data.batch;
					this.payouts = json.data.payouts || [];

					/*
					 * Completion toast: fire only on a processing -> terminal
					 * transition. Skipping the empty (first observation) and
					 * already-terminal cases avoids double-toasting batches that
					 * were complete before the drawer opened.
					 */
					if ( 'processing' === prevStatus && this._isTerminal( this.status ) ) {
						this._fireCompletionToast( this.status );
					}

				} catch ( err ) {
					console.error( '[batchDrawer] refresh failed:', err );
					this.errors.general = labels.loadFailedNetwork || '';
				}
			},

			_isTerminal( status ) {
				return [ 'completed', 'paid', 'partial', 'failed' ].includes( status );
			},

			/*
			 * Outcome notification: maps terminal status → toast { message, type }.
			 * Success / warning / error colors mirror the per-tile attention
			 * conventions used elsewhere in the flow.
			 */
			_fireCompletionToast( status ) {
				if ( typeof window.affwpToast !== 'function' || ! this.batch ) return;

				const labels = ( data.labels || {} );
				const id     = String( this.batchId );
				const total  = String( this.batch.total_count || 0 );
				const paid   = String( this.batch.processed_count || 0 );
				const failed = String( this.batch.failed_count || 0 );

				const fill = ( template ) => ( template || '' )
					.replace( /\{id\}/g, id )
					.replace( /\{total\}/g, total )
					.replace( /\{paid\}/g, paid )
					.replace( /\{failed\}/g, failed );

				let message, type;

				if ( 'completed' === status || 'paid' === status ) {
					message = fill( labels.toastCompleted );
					type    = 'success';
				} else if ( 'partial' === status ) {
					message = fill( labels.toastPartial );
					type    = 'warning';
				} else if ( 'failed' === status ) {
					message = fill( labels.toastFailed );
					type    = 'error';
				} else {
					return;
				}

				if ( ! message ) return;
				window.affwpToast( { message, type } );
			},

			/*
			 * Poll the batch drawer endpoint every 3s while the batch is
			 * still processing. The drawer reuses its own fetch path so
			 * there's no second endpoint to maintain. Terminal status halts
			 * the poll without any extra signal — status.processing flips
			 * to completed/partial/failed via recount() in the AS workers.
			 */
			_schedulePoll() {
				this._stopPolling();
				this._pollTimer = window.setTimeout( async () => {
					// Drawer closed, navigated away, or tab backgrounded — bail.
					if ( ! this.isOpen || document.hidden ) return;
					await this._refreshBatch();
					if ( 'processing' === this.status && this.isOpen && ! document.hidden ) {
						this._schedulePoll();
					}
				}, 3000 );
			},

			_stopPolling() {
				if ( this._pollTimer ) {
					window.clearTimeout( this._pollTimer );
					this._pollTimer = null;
				}
			},

			close() {
				const closedId = this.batchId;
				this._stopPolling();
				this.isOpen       = false;
				this.batchId      = 0;
				this.batch        = null;
				this.payouts      = [];
				this.errors       = {};
				this.statusFilter = 'all';
				this._syncUrl();

				// Restore focus to the element that triggered the drawer (see payout drawer for rationale).
				if ( this._returnFocusTo && this._returnFocusTo.isConnected ) {
					try {
						this._returnFocusTo.focus();
					} catch ( e ) {
						// Element may be unfocusable.
					}
				}
				this._returnFocusTo = null;

				/*
				 * Lifecycle event for third-party listeners. See payout
				 * drawer for the contract.
				 */
				window.dispatchEvent( new CustomEvent( 'affwp-drawer-closed', {
					detail: { drawer: 'batch', id: closedId }
				} ) );
			},

			/*
			 * Drawer trait calls save() on the primary button. We repurpose it
			 * for "Retry failed" when the batch has failures; otherwise close.
			 */
			save() {
				if ( this.canRetry ) {
					return this.retryFailed();
				}
				this.close();
			},

			goPrev() {
				if ( ! this.hasPrev ) return;
				const id = this.batchIds[ this.listIndex - 1 ];
				if ( id ) this.openFor( id );
			},

			goNext() {
				if ( ! this.hasNext ) return;
				const id = this.batchIds[ this.listIndex + 1 ];
				if ( id ) this.openFor( id );
			},

			/*
			 * Click a payout chip inside the batch drawer → close batch,
			 * open the payout drawer deep-linked via URL. The payout drawer
			 * reads `?payout_id=X` on its own init. Preserves `batch_id` in
			 * the URL so closing the payout drawer returns the admin to the
			 * same batch context.
			 */
			openPayout( payoutId ) {
				const payoutDrawer = Alpine.store( 'payoutDrawer' );
				if ( ! payoutDrawer ) {
					/*
					 * Shouldn't happen on the batches page (both drawers
					 * enqueue together) but fall back gracefully.
					 */
					return;
				}

				/*
				 * Pass origin context so the payout drawer can render a
				 * "← Back to Batch #N" breadcrumb and let the admin return
				 * without navigating via the URL. Only set when the swap
				 * is from batch → payout; admins landing on the payout
				 * drawer from the list table get no breadcrumb (there's
				 * nowhere to go "back" to).
				 */
				const returnTo = { type: 'batch', id: this.batchId };

				this.close();
				payoutDrawer.openFor( payoutId, { returnTo: returnTo } );
			},

			async retryFailed() {
				if ( ! this.canRetry ) return;
				const confirmTemplate = this.failedCount === 1
					? labels.retryConfirmSingular
					: labels.retryConfirmPlural;
				if ( ! window.confirm( fmt( confirmTemplate, this.failedCount ) ) ) {
					return;
				}

				this.isRetrying = true;

				try {
					const body = new URLSearchParams();
					body.append( 'action', 'affwp_batch_drawer_retry_failed' );
					body.append( 'batch_id', this.batchId );
					body.append( '_ajax_nonce', data.nonces.affwp_batch_drawer_retry_failed );

					const res  = await fetch( data.ajaxUrl, { method: 'POST', credentials: 'same-origin', body: body } );
					const json = await res.json();

					if ( json && json.success ) {
						const toastTemplate = json.data.retried === 1
							? labels.retryToastSingular
							: labels.retryToastPlural;
						window.dispatchEvent( new CustomEvent( 'affwp-toast', {
							detail: { message: fmt( toastTemplate, json.data.retried ), type: 'success' }
						} ) );
						/*
						 * Move the drawer to the new batch so the admin watches
						 * the retry's in-flight state immediately.
						 */
						this.openFor( json.data.new_batch_id );
					} else {
						window.dispatchEvent( new CustomEvent( 'affwp-toast', {
							detail: {
								message: ( json && json.data && json.data.message ) || labels.retryToastFailed || '',
								type: 'error',
							}
						} ) );
					}
				} catch ( err ) {
					window.dispatchEvent( new CustomEvent( 'affwp-toast', {
						detail: { message: labels.retryToastNetworkError || '', type: 'error' }
					} ) );
				} finally {
					this.isRetrying = false;
				}
			},

			_handleKeyNav( event ) {
				if ( ! this.isOpen ) return;

				/*
				 * Don't hijack keys while an input/etc. has focus, and skip
				 * when the payout drawer is open (it owns keyboard nav then).
				 */
				const payoutDrawer = Alpine.store( 'payoutDrawer' );
				if ( payoutDrawer && payoutDrawer.isOpen ) return;

				const el = document.activeElement;
				if ( el ) {
					const tag = el.tagName;
					if ( tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || el.isContentEditable ) {
						return;
					}
				}

				if ( event.metaKey || event.ctrlKey || event.altKey ) return;

				if ( event.key === 'ArrowUp' || event.key === 'k' ) {
					if ( this.hasPrev ) { event.preventDefault(); this.goPrev(); }
				} else if ( event.key === 'ArrowDown' || event.key === 'j' ) {
					if ( this.hasNext ) { event.preventDefault(); this.goNext(); }
				}
			},

			_syncUrl() {
				const url = new URL( window.location );

				// Enforce tab=batches so the drawer deep-link makes sense.
				if ( this.isOpen && this.batchId ) {
					url.searchParams.set( 'tab', 'batches' );
					url.searchParams.set( 'batch_id', this.batchId );
				} else {
					url.searchParams.delete( 'batch_id' );
				}

				window.history.replaceState( {}, '', url );
			},
		} );
	};

	if ( window.Alpine ) {
		registerStore();
	} else {
		document.addEventListener( 'alpine:init', registerStore );
	}

	/*
	 * Row-click intercept: batches list rows open the drawer instead of
	 * navigating. Also catches the "View" row action link.
	 */
	document.addEventListener( 'click', function ( event ) {
		const link = event.target.closest( 'a[href*="tab=batches"][href*="batch_id="]' );
		if ( ! link ) return;

		try {
			const url = new URL( link.href, window.location.origin );
			const id  = parseInt( url.searchParams.get( 'batch_id' ), 10 );
			if ( ! id ) return;

			/*
			 * Skip action-carrying links (e.g. retry_batch_failed nonced URLs)
			 * — those need to navigate so the admin-action handler can run.
			 */
			if ( url.searchParams.get( 'affwp_action' ) ) return;

			if ( window.Alpine && Alpine.store( 'batchDrawer' ) ) {
				event.preventDefault();
				Alpine.store( 'batchDrawer' ).openFor( id );
			}
		} catch ( e ) {
			// Malformed href — let the browser handle it.
		}
	} );

	/*
	 * Whole-row click: anywhere on the row opens the drawer, except clicks
	 * on interactive controls or on the sticky Actions column (which has
	 * its own buttons + links). Mouse-only convenience; keyboard users
	 * still reach the drawer via the kebab's View link.
	 */
	document.addEventListener( 'click', function ( event ) {
		if ( event.target.closest( 'a, button, input, label, select, textarea, .check-column, .column-actions' ) ) {
			return;
		}

		const row = event.target.closest( 'tr[data-batch-id]' );
		if ( ! row ) { return; }

		const id = parseInt( row.dataset.batchId, 10 );
		if ( ! id ) { return; }

		if ( window.Alpine && Alpine.store( 'batchDrawer' ) ) {
			Alpine.store( 'batchDrawer' ).openFor( id );
		}
	} );

} )();
