import { TryOnRejectedError } from './tryOnErrors';
/**
 * Product analytics for the WooCommerce widget.
 *
 * Everything goes through the bootstrap's `aisthetixTrackProductEvent`, which
 * owns the consent gate and the transport. There is deliberately no fallback
 * path here, unlike `events.ts`'s attribution fallback: attribution can fall
 * back to a direct POST because the merchant's own opt-in is a legitimate basis
 * for it, but product analytics needs the shopper's CMP grant, and the only
 * code that can read the CMP is the bootstrap. A fallback would be a way to
 * emit without ever consulting it.
 *
 * Nothing here can throw into a caller. A widget that breaks because analytics
 * broke is a worse outcome than no analytics.
 */
import type { WidgetConfig } from './types';

declare global {
	interface Window {
		aisthetixTrackProductEvent?: ( event: string, props?: Record< string, unknown > ) => boolean;
		aisthetixProductAnalyticsAllowed?: () => boolean;
	}
}

/**
 * Whether product analytics may run at all right now.
 *
 * Absent bootstrap means no: the widget can be loaded without it (the bundle is
 * injected by a script tag), and without the bootstrap there is nothing that
 * has read the merchant's CMP.
 */
export function productAnalyticsAllowed(): boolean {
	try {
		return window.aisthetixProductAnalyticsAllowed?.() === true;
	} catch {
		return false;
	}
}

/**
 * Emit one product-analytics event.
 *
 * @param event - A contract event name
 * @param props - Event properties; undeclared ones are dropped by the bootstrap
 * @returns Whether it was sent
 */
export function trackProductEvent( event: string, props: Record< string, unknown > = {} ): boolean {
	try {
		return window.aisthetixTrackProductEvent?.( event, props ) === true;
	} catch {
		return false;
	}
}

/**
 * A correlation id for one modal run.
 *
 * Regenerated every time the modal opens, so a shopper's two try-ons are two
 * funnels rather than one that appears to loop. Not persisted and not an
 * identity: it lasts as long as the modal and is gone.
 */
export function newRunId(): string {
	try {
		if ( window.crypto && 'randomUUID' in window.crypto ) {
			return window.crypto.randomUUID();
		}
	} catch {
		/* fall through */
	}
	return `r${ Date.now().toString( 36 ) }${ Math.random().toString( 36 ).slice( 2, 10 ) }`;
}

/**
 * Map a caught try-on failure to a contract reason.
 *
 * The widget catches Errors whose messages come from `fetch`, from the theme
 * and from our own API client — free text, written by whoever threw it. None of
 * it may become an analytics property, so the failure becomes an enum member
 * and the message goes no further than the shopper's own screen.
 *
 * Ordered most-specific first: "failed to fetch image" also contains "fetch",
 * and reporting a garment image the theme could not serve as a network failure
 * would send us looking at the wrong system.
 */
export function tryonFailureReason( error: unknown ): string {
  if (error instanceof TryOnRejectedError) {
    if (error.code === 'photo_body_hidden' || error.code === 'bad_image') return 'invalid_photo';
    return error.code === 'quality_failed' ? 'provider_error' : 'provider_refused';
  }
	const raw = error instanceof Error ? error.message.toLowerCase() : String( error ?? '' ).toLowerCase();
	if ( ! raw ) return 'unexpected';
	if ( raw.includes( 'failed to fetch image' ) || raw.includes( 'image load' ) ) return 'image_load_failed';
	if ( raw.includes( 'abort' ) || raw.includes( 'timed out' ) || raw.includes( 'timeout' ) ) return 'timeout';
	if ( raw.includes( 'failed to fetch' ) || raw.includes( 'network' ) || raw.includes( 'cors' ) ) return 'network';
	if ( raw.includes( '413' ) || raw.includes( 'too large' ) ) return 'payload_too_large';
	if ( raw.includes( '429' ) || raw.includes( 'rate limit' ) ) return 'rate_limited';
	if (
		raw.includes( '502' ) ||
		raw.includes( '503' ) ||
		raw.includes( '504' ) ||
		raw.includes( 'unavailable' )
	) {
		return 'engine_unavailable';
	}
	if ( raw.includes( 'invalid photo' ) || raw.includes( 'unsupported image' ) ) return 'invalid_photo';
	if ( raw.includes( 'refused' ) || raw.includes( 'safety' ) ) return 'provider_refused';
	if ( raw.includes( 'job failed' ) || raw.includes( 'api error' ) ) return 'provider_error';
	return 'unexpected';
}

/** Which reasons are worth offering a retry for. */
export function isRetryableTryonReason( reason: string ): boolean {
	return (
		reason === 'network' ||
		reason === 'timeout' ||
		reason === 'engine_unavailable' ||
		reason === 'provider_error' ||
		reason === 'rate_limited'
	);
}

/** Replay configuration, as the plugin serves it. */
export interface ReplaySettings {
	key: string;
	host: string;
	enabled: boolean;
	environment: string;
	store: string;
}

/**
 * Read replay settings out of the injected config.
 *
 * Absent or incomplete means replay never runs — and, because the SDK is
 * fetched lazily, means no network request is made either.
 */
export function replaySettings( config: WidgetConfig ): ReplaySettings | null {
	const analytics = ( config as unknown as { analytics?: Record< string, unknown > } ).analytics;
	if ( ! analytics ) return null;
	const key = typeof analytics.replayKey === 'string' ? analytics.replayKey : '';
	const host = typeof analytics.ingestHost === 'string' ? analytics.ingestHost : '';
	const store = typeof analytics.storeId === 'string' ? analytics.storeId : '';
	if ( ! key || ! host || analytics.replayEnabled !== true ) return null;
	// A recording that names no store is one nobody can find in PostHog and
	// nobody can attribute to a merchant — worse than no recording, because it
	// was still taken from a shopper. The plugin already refuses to emit a block
	// without one; this is the second net.
	if ( ! store ) return null;
	return {
		key,
		host,
		enabled: true,
		environment: typeof analytics.environment === 'string' ? analytics.environment : 'production',
		store,
	};
}
