import { useCallback, useEffect, useRef, useState } from 'react';

import {
	WEB_FETCH_CONSENT_REQUIRED,
	WEB_FETCH_NOT_CONNECTED,
	WEB_FETCH_NOT_REGISTERED,
	weBFetchErrorMessage,
	weBFetchIsRetryable,
} from '../fetch/Fetch';
import { WeBWorkspaceSaveStatusResponse } from '../fetch/types';

/** What the element answers a successful save with */
export type WeBWorkspaceSaveOutcome = {
	/** True when the cloud already held an identical latest save */
	deduplicated: boolean;
	/** The save's id — what `saveStatus` is polled with while `processing` */
	saveId: string;
	/** ISO-8601; null while the cloud is still processing the save */
	savedAt: string | null;
	/** `processing` when the cloud has the bytes but has not yet validated them */
	status: 'complete' | 'processing';
};

/**
 * A save failure as the banner shows it: the message, a retry where one can
 * help, and a link where the remedy is somewhere else (the cloud's graph area,
 * for consent).
 */
export type WeBSaveFailure = {
	message: string;
	retry: ( () => void ) | null;
	link: { href: string; label: string } | null;
};

/** The path under the cloud dashboard where consent is given */
const WEB_CLOUD_GRAPH_PATH = '/graph';

/** How often a `processing` save is asked about */
export const WEB_SAVE_STATUS_POLL_MS = 3000;

/** What the banner says when the cloud marks a save failed without a message */
const WEB_SAVE_FAILED = 'The cloud could not complete the save.';

type Args = {
	/**
	 * Whether a save can be attempted at all: connected for this product, and
	 * a workspace with a data source (not the demo). Nothing is sent otherwise.
	 */
	canSave: boolean;
	/** The cloud dashboard, for the consent link; '' when unknown */
	dashUrl: string;
	/** Called with the server's time after a successful save */
	markSaved: ( savedAt: string ) => void;
	/** Raised when a save fails; null clears it */
	onFailure: ( failure: WeBSaveFailure | null ) => void;
	/** Re-checks `connect/status`; called when the server says we are not connected */
	refreshConnect: () => void;
	/** The element's status poll; absent on the demo path */
	saveStatus?: (
		saveId: string
	) => Promise< WeBWorkspaceSaveStatusResponse >;
	/** The element's save; absent on the demo path */
	workspaceSave?: () => Promise< WeBWorkspaceSaveOutcome >;
};

/**
 * The chrome's half of a save: one request at a time, and the failure sorted
 * into what the user can do about it.
 *
 * The element makes the request and records the id (`__workspaceSave`); this
 * hook decides what a rejection means, because that depends on things the
 * element does not have — the connect state machine and the dashboard URL:
 *
 * - `not-connected` / `not-registered`: the token was revoked or the domain
 *   cleared under us. The message is shown and `connect/status` re-checked so
 *   the button drops back to Register without a reload. No retry — nothing
 *   changes until the user registers again.
 * - `consent_required`: given once, in the cloud's graph area. The banner
 *   links there.
 * - everything else: the server's message, with a retry where the fetch layer
 *   says one can help (`unavailable`, unreachable).
 *
 * A save the server answers `processing` for is not finished: the cloud has
 * the bytes and is validating them on a queue. The hook stays `saving`, sets
 * `processing`, and polls `saveStatus` every few seconds until the cloud says
 * `complete` (→ `markSaved`) or `failed` (→ the banner, with the cloud's
 * message and a retry). The poll stops with the component.
 *
 * @param args
 * @param args.canSave
 * @param args.dashUrl
 * @param args.markSaved
 * @param args.onFailure
 * @param args.refreshConnect
 * @param args.saveStatus
 * @param args.workspaceSave
 * @return `save` — a no-op while one is in flight or when saving is not
 *         possible — whether one is in flight, and whether the cloud is still
 *         processing it
 */
export function useWorkspaceSave( {
	canSave,
	dashUrl,
	markSaved,
	onFailure,
	refreshConnect,
	saveStatus,
	workspaceSave,
}: Args ): { processing: boolean; save: () => void; saving: boolean } {
	const [ saving, setSaving ] = useState< boolean >( false );
	const [ processing, setProcessing ] = useState< boolean >( false );

	/**
	 * Refs for what the in-flight promise reads on resolution, so a re-render
	 * during the request does not bind the handlers to stale values — and so
	 * `save` itself keeps one identity for the keyboard listener.
	 */
	const latest = useRef( {
		canSave,
		dashUrl,
		markSaved,
		onFailure,
		refreshConnect,
		saveStatus,
		saving,
		workspaceSave,
	} );

	useEffect( () => {
		latest.current = {
			canSave,
			dashUrl,
			markSaved,
			onFailure,
			refreshConnect,
			saveStatus,
			saving,
			workspaceSave,
		};
	} );

	/** True after unmount, so a late response does not set state */
	const disposed = useRef< boolean >( false );

	/** The pending status poll, so unmount can cancel it */
	const pollId = useRef< number >( 0 );

	useEffect( () => {
		disposed.current = false;

		return () => {
			disposed.current = true;
			window.clearTimeout( pollId.current );
		};
	}, [] );

	const save = useCallback( () => {
		const now = latest.current;

		if ( ! now.canSave || ! now.workspaceSave || now.saving ) {
			return;
		}

		/** The request is over, one way or the other */
		const settle = () => {
			setSaving( false );
			setProcessing( false );
		};

		/**
		 * A rejection — from the save or from a poll — as a banner
		 * @param reason
		 */
		const fail = ( reason: unknown ) => {
			settle();

			const message = weBFetchErrorMessage( reason );
			const current = latest.current;

			if (
				message === WEB_FETCH_NOT_CONNECTED ||
				message === WEB_FETCH_NOT_REGISTERED
			) {
				current.onFailure( { message, retry: null, link: null } );
				current.refreshConnect();
				return;
			}

			if ( message === WEB_FETCH_CONSENT_REQUIRED ) {
				current.onFailure( {
					message,
					retry: null,
					link: current.dashUrl
						? {
								href:
									current.dashUrl.replace( /\/$/, '' ) +
									WEB_CLOUD_GRAPH_PATH,
								label: 'Open your graph area',
						  }
						: null,
				} );
				return;
			}

			current.onFailure( {
				message,
				retry: weBFetchIsRetryable( reason ) ? save : null,
				link: null,
			} );
		};

		/**
		 * The save is stored: the cloud's time, or ours when it gave none
		 * @param savedAt
		 */
		const complete = ( savedAt: string | null ) => {
			settle();
			latest.current.markSaved( savedAt ?? new Date().toISOString() );
		};

		/**
		 * One status request, and the next one if the cloud is still at it
		 * @param saveId
		 */
		const poll = ( saveId: string ) => {
			const status = latest.current.saveStatus;

			if ( ! status ) {
				//nothing to ask with: call it saved rather than spin for ever
				complete( null );
				return;
			}

			status( saveId ).then(
				( response ) => {
					if ( disposed.current ) {
						return;
					}

					if ( response.status === 'complete' ) {
						complete( response.savedAt );
						return;
					}

					if ( response.status === 'failed' ) {
						settle();
						latest.current.onFailure( {
							message: response.failureMessage || WEB_SAVE_FAILED,
							retry: save,
							link: null,
						} );
						return;
					}

					pollId.current = window.setTimeout(
						() => poll( saveId ),
						WEB_SAVE_STATUS_POLL_MS
					);
				},
				( reason: unknown ) => {
					if ( ! disposed.current ) {
						fail( reason );
					}
				}
			);
		};

		setSaving( true );
		now.onFailure( null );

		now.workspaceSave().then(
			( outcome ) => {
				if ( disposed.current ) {
					return;
				}

				if ( outcome.status === 'processing' ) {
					setProcessing( true );
					pollId.current = window.setTimeout(
						() => poll( outcome.saveId ),
						WEB_SAVE_STATUS_POLL_MS
					);
					return;
				}

				complete( outcome.savedAt );
			},
			( reason: unknown ) => {
				if ( ! disposed.current ) {
					fail( reason );
				}
			}
		);
	}, [] );

	return { processing, save, saving };
}
