import apiFetch from '@wordpress/api-fetch';
import { __, _n, sprintf } from '@wordpress/i18n';
import type { AdminBootstrap, Station } from '@shared/types';

const cfg = ( window.StreamSeraAdmin || {} ) as AdminBootstrap;
apiFetch.use( apiFetch.createRootURLMiddleware( cfg.restUrl ) );
apiFetch.use( apiFetch.createNonceMiddleware( cfg.nonce ) );

export const config = cfg;

type Notify = ( message: string, status?: string ) => void;

/**
 * Readable message for a failed request (falls back to a generic sentence).
 * @param e
 */
export function errorMessage( e: any ): string {
	return (
		( e && typeof e.message === 'string' && e.message ) ||
		__( 'That did not work. Please try again.', 'streamsera' )
	);
}

/**
 * Run a list action (streamsera/v1/bulk) and tell the user what happened: how many items changed,
 * how many were skipped (not allowed, or a station without a stream URL), or why it failed.
 * @param type
 * @param ids
 * @param action
 * @param notify
 * @param doneText
 */
export async function runBulk(
	type: 'station' | 'player',
	ids: number[],
	action: 'delete' | 'publish' | 'draft' | 'duplicate',
	notify: Notify,
	doneText: ( n: number ) => string
): Promise< boolean > {
	try {
		const r: any = await apiFetch( {
			path: '/streamsera/v1/bulk',
			method: 'POST',
			data: { type, ids, action },
		} );
		const done = Array.isArray( r?.done ) ? r.done.length : ids.length;
		const skipped = Array.isArray( r?.skipped ) ? r.skipped.length : 0;
		if ( done ) {
			notify( doneText( done ) );
		}
		if ( skipped ) {
			notify(
				'publish' === action && 'station' === type
					? sprintf(
							/* translators: %d: number of skipped items. */
							_n(
								'%d item was skipped: you are not allowed to publish it, or the station has no stream URL yet.',
								'%d items were skipped: you are not allowed to publish them, or a station has no stream URL yet.',
								skipped,
								'streamsera'
							),
							skipped
					  )
					: sprintf(
							/* translators: %d: number of skipped items. */
							_n(
								'%d item was skipped: you are not allowed to change it.',
								'%d items were skipped: you are not allowed to change them.',
								skipped,
								'streamsera'
							),
							skipped
					  ),
				done ? 'warning' : 'error'
			);
		}
		return done > 0;
	} catch ( e ) {
		notify( errorMessage( e ), 'error' );
		return false;
	}
}

/**
 * Whether a failed apiFetch call was rejected for lack of permission (401/403, or the
 * WP REST "rest_forbidden"/"rest_cannot_*" codes). Pages use it to show a clear
 * "you don't have permission" state instead of an endless spinner or an empty list.
 * @param e
 */
export function isPermissionError( e: any ): boolean {
	if ( ! e ) {
		return false;
	}
	const status = e?.data?.status ?? e?.status;
	if ( status === 401 || status === 403 ) {
		return true;
	}
	const code = String( e?.code || '' );
	return (
		code === 'rest_forbidden' ||
		code === 'rest_forbidden_context' ||
		code === 'rest_cookie_invalid_nonce' ||
		code === 'rest_not_logged_in' ||
		/^rest_cannot_/.test( code )
	);
}

/**
 * GET every page of a collection (per_page=100, following X-WP-TotalPages), so lists and pickers
 * show all records however many there are.
 * @param path
 */
export async function fetchAll< T = any >( path: string ): Promise< T[] > {
	const sep = path.includes( '?' ) ? '&' : '?';
	const out: T[] = [];
	let page = 1;
	let pages = 1;
	do {
		const res = ( await apiFetch( {
			path: `${ path }${ sep }per_page=100&page=${ page }`,
			parse: false,
		} ) ) as Response;
		pages = Math.max(
			1,
			parseInt( res.headers.get( 'X-WP-TotalPages' ) || '1', 10 ) || 1
		);
		const rows = ( await res.json() ) as T[];
		out.push( ...( Array.isArray( rows ) ? rows : [] ) );
		page++;
	} while ( page <= pages && page <= 100 );
	return out;
}

/**
 * Collection query for stations / players: users who may edit them get drafts in the edit context;
 * everyone else (read-only roles) gets the published items in the view context, with `title.raw`
 * filled from the rendered title so callers can use the same shape.
 * @param fields
 */
function listQuery( fields?: string ) {
	const canEdit = ( cfg as any ).canEditPosts !== false;
	const base = canEdit
		? 'status=publish,draft&context=edit'
		: 'status=publish&context=view';
	return fields ? `${ base }&_fields=${ fields }` : base;
}

function withRawTitle( rows: any[] ): any[] {
	if ( ( cfg as any ).canEditPosts !== false ) {
		return rows;
	}
	const el = document.createElement( 'textarea' );
	return rows.map( ( r ) => {
		if ( r?.title && r.title.raw === undefined ) {
			el.innerHTML = String( r.title.rendered || '' );
			r.title = { ...r.title, raw: el.value };
		}
		return r;
	} );
}

const STATION_FIELDS =
	'id,title,slug,content,status,meta,featured_media,date,modified,streamsera_image';

export const stations = {
	list: () =>
		fetchAll< any >(
			`/wp/v2/streamsera-stations?${ listQuery( STATION_FIELDS ) }`
		).then( withRawTitle ),
	get: ( id ) =>
		apiFetch< any >( {
			path: `/wp/v2/streamsera-stations/${ id }?context=edit`,
		} ),
	create: ( data ) =>
		apiFetch< any >( {
			path: '/wp/v2/streamsera-stations',
			method: 'POST',
			data,
		} ),
	update: ( id, data ) =>
		apiFetch< any >( {
			path: `/wp/v2/streamsera-stations/${ id }`,
			method: 'POST',
			data,
		} ),
	remove: ( id ) =>
		apiFetch< any >( {
			path: `/wp/v2/streamsera-stations/${ id }?force=true`,
			method: 'DELETE',
		} ),
};

export const players = {
	list: () =>
		fetchAll< any >( `/wp/v2/streamsera-players?${ listQuery() }` ).then(
			withRawTitle
		),
	get: ( id ) =>
		apiFetch< any >( {
			path: `/wp/v2/streamsera-players/${ id }?context=edit`,
		} ),
	create: ( data ) =>
		apiFetch< any >( {
			path: '/wp/v2/streamsera-players',
			method: 'POST',
			data,
		} ),
	update: ( id, data ) =>
		apiFetch< any >( {
			path: `/wp/v2/streamsera-players/${ id }`,
			method: 'POST',
			data,
		} ),
	remove: ( id ) =>
		apiFetch< any >( {
			path: `/wp/v2/streamsera-players/${ id }?force=true`,
			method: 'DELETE',
		} ),
};

export interface PlayerPlace {
	type:
		| 'post'
		| 'template'
		| 'pattern'
		| 'widget'
		| 'listen'
		| 'sitewide'
		| string;
	label: string;
	title: string;
	url: string;
	edit: string;
}

export const ss = {
	/** Settings → Streams & now playing: every station's source and the headline numbers. */
	streamsStatus: () =>
		apiFetch< any >( { path: '/streamsera/v1/streams/status' } ),
	/**
	 * Read one station's track info now.
	 * @param id
	 */
	streamTest: ( id: number ) =>
		apiFetch< any >( {
			path: `/streamsera/v1/streams/${ id }/test`,
			method: 'POST',
		} ),
	detect: ( url ) =>
		apiFetch< any >( {
			path: '/streamsera/v1/detect',
			method: 'POST',
			data: { url },
		} ),
	nowPlaying: ( station, force = false ) =>
		apiFetch< any >( {
			path: `/streamsera/v1/now-playing/${ station }${
				force ? '?force=1' : ''
			}`,
		} ),
	getSettings: () => apiFetch< any >( { path: '/streamsera/v1/settings' } ),
	/**
	 * Where each player is placed on the site: { [playerId]: [{ type, label, title, url, edit }] }.
	 * @param refresh
	 */
	usage: ( refresh = false ) =>
		apiFetch< Record< string, PlayerPlace[] > >( {
			path: `/streamsera/v1/players/usage${
				refresh ? '?refresh=1' : ''
			}`,
		} ),
	saveSettings: ( data ) =>
		apiFetch< any >( {
			path: '/streamsera/v1/settings',
			method: 'POST',
			data,
		} ),
	/**
	 * Dashboard numbers + last known status per station. Never blocks on stream servers.
	 * `refresh` hands stale stations to the serialized background worker (one fetch at a time).
	 * @param refresh
	 */
	overview: ( refresh = false ) =>
		apiFetch< any >( {
			path: `/streamsera/v1/overview${ refresh ? '?refresh=1' : '' }`,
		} ),
	status: () => apiFetch< any >( { path: '/streamsera/v1/tools/status' } ),
	clearCache: ( recent = false ) =>
		apiFetch< any >( {
			path: '/streamsera/v1/tools/clear-cache',
			method: 'POST',
			data: { recent },
		} ),
	exportAll: () => apiFetch< any >( { path: '/streamsera/v1/tools/export' } ),
	importAll: (
		data: any,
		/** settings | stations | players | any add-on section (playlists, shows …): false skips it. */
		what: Record< string, boolean > = {}
	) =>
		apiFetch< any >( {
			path: '/streamsera/v1/tools/import',
			method: 'POST',
			data: {
				data,
				settings: true,
				stations: true,
				players: true,
				...what,
			},
		} ),
	notices: () => apiFetch< any >( { path: '/streamsera/v1/notices' } ),
	/**
	 * Bulk delete / publish / draft for our post types.
	 * @param type
	 * @param ids
	 * @param action
	 */
	bulk: (
		type: string,
		ids: number[],
		action: 'delete' | 'publish' | 'draft' | 'duplicate'
	) =>
		apiFetch< any >( {
			path: '/streamsera/v1/bulk',
			method: 'POST',
			data: { type, ids, action },
		} ),
	/**
	 * Add a player to an existing page, or create a page for it (Embed dialog).
	 * @param data
	 * @param data.player
	 * @param data.page_id
	 * @param data.title
	 * @param data.status
	 * @param data.position
	 */
	embedToPage: ( data: {
		player: number;
		page_id?: number;
		title?: string;
		status?: 'publish' | 'draft';
		position?: 'append' | 'prepend';
	} ) =>
		apiFetch< any >( {
			path: '/streamsera/v1/embed/page',
			method: 'POST',
			data,
		} ),
	/**
	 * Pages/posts for the "Add to page" picker.
	 * @param search
	 */
	searchPages: ( search: string ) =>
		apiFetch< any[] >( {
			path: `/wp/v2/pages?search=${ encodeURIComponent(
				search
			) }&per_page=20&status=publish,draft,private&_fields=id,title,status,link&context=edit`,
		} ),
	dismissNotice: ( id: string ) =>
		apiFetch< any >( {
			path: '/streamsera/v1/notices',
			method: 'POST',
			data: { id },
		} ),
	media: ( id ) =>
		apiFetch< any >( {
			path: `/wp/v2/media/${ id }?_fields=id,source_url,media_details`,
		} ),
};

/**
 * Convert a station REST record into the public shape the Player expects.
 * @param rec
 * @param logoUrl
 */
export function stationToPublic( rec: any, logoUrl = '' ): Station {
	const m = rec?.meta || {};
	return {
		id: rec?.id || 0,
		name: rec?.title?.raw || rec?.title?.rendered || '',
		slogan: m._streamsera_slogan || '',
		description: rec?.content?.raw || '',
		genre: m._streamsera_genre || '',
		frequency: m._streamsera_frequency || '',
		website: m._streamsera_website || '',
		logo: logoUrl,
		streams: Array.isArray( m._streamsera_streams )
			? m._streamsera_streams
			: [],
		social: m._streamsera_social || {},
		email: m._streamsera_email || '',
		phone: m._streamsera_phone || '',
		// External player files (builder preview): same URLs the frontend payload carries.
		m3u: rec?.id && cfg.feeds?.m3u ? feedUrl( cfg.feeds.m3u, rec.id ) : '',
		pls: rec?.id && cfg.feeds?.pls ? feedUrl( cfg.feeds.pls, rec.id ) : '',
	};
}

/**
 * Swap the placeholder station id (0) in a feed URL pattern for a real station.
 * @param pattern
 * @param id
 */
export function feedUrl( pattern: string, id: number ): string {
	return pattern
		.replace( /([?&]streamsera_(?:playlist|overlay)=)0/, `$1${ id }` )
		.replace( /\/([a-z-]+)\/0(\/|$)/, `/$1/${ id }$2` );
}

/**
 * Whether a now-playing payload is a placeholder the background worker is still filling in.
 * @param np
 */
export const isPendingNp = ( np: any ) => !! np && ( np.pending || np.stale );

/**
 * Re-read the overview while stations are still being refreshed in the background.
 * Polls gently (one request every `interval` ms, at most `max` times) and stops as soon as
 * nothing is pending. Returns a cancel function for unmount.
 * @param onUpdate
 * @param root0
 * @param root0.interval
 * @param root0.max
 */
export function pollOverview(
	onUpdate: ( overview: any ) => void,
	{ interval = 4000, max = 8 }: { interval?: number; max?: number } = {}
) {
	let cancelled = false;
	let timer: number | undefined;
	let runs = 0;
	const tick = async () => {
		if ( cancelled || runs >= max ) {
			return;
		}
		runs++;
		try {
			const o = await ss.overview();
			if ( cancelled ) {
				return;
			}
			onUpdate( o );
			const stillPending =
				( o?.pending || 0 ) > 0 ||
				Object.values( o?.status || {} ).some( isPendingNp );
			if ( ! stillPending ) {
				return;
			}
		} catch ( e ) {
			// Transient failure: keep trying within the budget.
		}
		timer = window.setTimeout( tick, interval );
	};
	timer = window.setTimeout( tick, interval );
	return () => {
		cancelled = true;
		window.clearTimeout( timer );
	};
}

/**
 * Read now-playing for one station, then re-read a few times while the background worker
 * is still fetching it. Never asks the server to fetch inline (use `ss.nowPlaying( id, true )`
 * for the explicit "Test now" action).
 * @param station
 * @param onUpdate
 * @param root0
 * @param root0.interval
 * @param root0.max
 */
export function watchNowPlaying(
	station: number | string,
	onUpdate: ( np: any ) => void,
	{ interval = 4000, max = 6 }: { interval?: number; max?: number } = {}
) {
	let cancelled = false;
	let timer: number | undefined;
	let runs = 0;
	const tick = async () => {
		if ( cancelled || runs >= max ) {
			return;
		}
		runs++;
		try {
			const np = await ss.nowPlaying( station );
			if ( cancelled ) {
				return;
			}
			onUpdate( np );
			if ( ! isPendingNp( np ) ) {
				return;
			}
		} catch ( e ) {
			return;
		}
		timer = window.setTimeout( tick, interval );
	};
	tick();
	return () => {
		cancelled = true;
		window.clearTimeout( timer );
	};
}

/* ---------------------------------------------------------------- preview */

/**
 * URL of the stand-alone preview page for a saved player (logged-in editors only).
 * @param player
 * @param theme
 * @param device
 */
export function previewUrl(
	player: number | string,
	theme: '' | 'light' | 'dark' = '',
	device: '' | 'tablet' | 'phone' = ''
) {
	const base =
		( config as any ).previewUrl ||
		`${ ( config.siteUrl || '' ).replace(
			/\/$/,
			''
		) }/?streamsera_preview=0`;
	const url = new URL( base, window.location.href );
	url.searchParams.set( 'streamsera_preview', String( player ) );
	if ( theme ) {
		url.searchParams.set( 'theme', theme );
	} else {
		url.searchParams.delete( 'theme' );
	}
	if ( device ) {
		url.searchParams.set( 'device', device );
	} else {
		url.searchParams.delete( 'device' );
	}
	return url.toString();
}

/**
 * Open the preview in a new browser tab (one tab per player; re-used on repeat clicks).
 * @param player
 * @param theme
 * @param device
 */
export function openPreview(
	player: number | string,
	theme: '' | 'light' | 'dark' = '',
	device: '' | 'tablet' | 'phone' = ''
) {
	const win = window.open(
		previewUrl( player, theme, device ),
		`streamsera-preview-${ player }`,
		'noopener'
	);
	if ( win ) {
		win.focus();
	}
	return win;
}
