import { addQueryArgs } from '@wordpress/url';
import deprecated from '@wordpress/deprecated';
import { useSelect } from '@wordpress/data';
import { useMemo } from '@wordpress/element';
import { store as coreStore } from '../';
import type { Options } from './use-entity-record';
import type { Status } from './constants';
import { getResolutionStatus } from './utils';
import { unlock } from '../lock-unlock';
import { getNormalizedCommaSeparable } from '../utils';

export interface EntityRecordsResolution< RecordType > {
	/** The requested entity records */
	records: RecordType[] | null;

	/**
	 * Is the record still being resolved?
	 */
	isResolving: boolean;

	/**
	 * Has the resolution started?
	 */
	hasStarted: boolean;

	/**
	 * Is the record resolved by now?
	 */
	hasResolved: boolean;

	/** Resolution status */
	status: Status;

	/**
	 * The total number of available items (if not paginated).
	 */
	totalItems: number | null;

	/**
	 * The total number of pages.
	 */
	totalPages: number | null;
}

export type WithPermissions< RecordType > = RecordType & {
	permissions: { delete: boolean; update: boolean };
};

interface EntityRecordsWithPermissionsResolution< RecordType >
	extends Omit< EntityRecordsResolution< RecordType >, 'records' > {
	/** The requested entity records with permissions */
	records: WithPermissions< RecordType >[] | null;
}

const EMPTY_ARRAY = [];

/**
 * Resolves the specified entity records.
 *
 * @since 6.1.0 Introduced in WordPress core.
 *
 * @param    kind      Kind of the entity, e.g. `root` or a `postType`. See rootEntitiesConfig in ../entities.ts for a list of available kinds.
 * @param    name      Name of the entity, e.g. `plugin` or a `post`. See rootEntitiesConfig in ../entities.ts for a list of available names.
 * @param    queryArgs Optional HTTP query description for how to fetch the data, passed to the requested API endpoint.
 * @param    options   Optional hook options.
 * @example
 * ```js
 * import { useEntityRecords } from '@wordpress/core-data';
 *
 * function PageTitlesList() {
 *   const { records, isResolving } = useEntityRecords( 'postType', 'page' );
 *
 *   if ( isResolving ) {
 *     return 'Loading...';
 *   }
 *
 *   return (
 *     <ul>
 *       {records.map(( page ) => (
 *         <li>{ page.title }</li>
 *       ))}
 *     </ul>
 *   );
 * }
 *
 * // Rendered in the application:
 * // <PageTitlesList />
 * ```
 *
 * In the above example, when `PageTitlesList` is rendered into an
 * application, the list of records and the resolution details will be retrieved from
 * the store state using `getEntityRecords()`, or resolved if missing.
 *
 * @return Entity records data.
 * @template RecordType
 */
export default function useEntityRecords< RecordType >(
	kind: string,
	name: string,
	queryArgs: Record< string, unknown > = {},
	options: Options = { enabled: true }
): EntityRecordsResolution< RecordType > {
	// Serialize queryArgs to a string that can be safely used as a React dep.
	// We can't just pass queryArgs as one of the deps, because if it is passed
	// as an object literal, then it will be a different object on each call even
	// if the values remain the same.
	const queryAsString = addQueryArgs( '', queryArgs );

	const { records, totalItems, totalPages, ...rest } = useSelect(
		( select ) => {
			if ( ! options.enabled ) {
				return {
					// Avoiding returning a new reference on every execution.
					records: EMPTY_ARRAY,
					totalItems: null,
					totalPages: null,
					...getResolutionStatus(),
				};
			}

			const storeSelectors = select( coreStore );
			const resolutionStatus = storeSelectors.getResolutionState(
				'getEntityRecords',
				[ kind, name, queryArgs ]
			)?.status;

			return {
				records: storeSelectors.getEntityRecords(
					kind,
					name,
					queryArgs
				) as RecordType[] | null,
				totalItems: storeSelectors.getEntityRecordsTotalItems(
					kind,
					name,
					queryArgs
				),
				totalPages: storeSelectors.getEntityRecordsTotalPages(
					kind,
					name,
					queryArgs
				),
				...getResolutionStatus( resolutionStatus ),
			};
		},
		[ kind, name, queryAsString, options.enabled ]
	);

	return {
		records,
		totalItems,
		totalPages,
		...rest,
	};
}

export function useDeprecatedEntityRecords(
	kind: string,
	name: string,
	queryArgs: any,
	options: any
) {
	deprecated( `wp.data.__experimentalUseEntityRecords`, {
		alternative: 'wp.data.useEntityRecords',
		since: '6.1',
	} );
	return useEntityRecords( kind, name, queryArgs, options );
}

export function useEntityRecordsWithPermissions< RecordType >(
	kind: string,
	name: string,
	queryArgs: Record< string, unknown > = {},
	options: Options = { enabled: true }
): EntityRecordsWithPermissionsResolution< RecordType > {
	const entityConfig = useSelect(
		( select ) => select( coreStore ).getEntityConfig( kind, name ),
		[ kind, name ]
	);
	const { records: data, ...ret } = useEntityRecords(
		kind,
		name,
		{
			...queryArgs,
			// If _fields is provided, we need to include _links in the request for permission caching to work.
			...( queryArgs._fields
				? {
						_fields: [
							...new Set( [
								...( getNormalizedCommaSeparable(
									queryArgs._fields
								) || [] ),
								'_links',
							] ),
						].join(),
				  }
				: {} ),
		},
		options
	);
	const ids = useMemo(
		() =>
			data?.map(
				// @ts-expect-error `data` is `unknown[]`, so the callback signature does not line up.
				( record: RecordType ) => record[ entityConfig?.key ?? 'id' ]
			) ?? [],
		[ data, entityConfig?.key ]
	);

	const permissions = useSelect(
		( select ) => {
			const { getEntityRecordsPermissions } = unlock(
				select( coreStore )
			);
			return getEntityRecordsPermissions( kind, name, ids );
		},
		[ ids, kind, name ]
	);

	const dataWithPermissions = useMemo(
		() =>
			data?.map( ( record, index ) => ( {
				// @ts-expect-error `record` is `unknown`, which cannot be spread.
				...record,
				permissions: permissions[ index ],
			} ) ) ?? [],
		[ data, permissions ]
	);

	return { records: dataWithPermissions, ...ret };
}
