import { expect, Page, Request } from '@playwright/test';
import { siteConfig } from '../config/siteConfig';

export type GaEvent = {
    name: string;
    id?: string;
    requestUrl: string;
    params: URLSearchParams;
};

const requiredEventParams: Record<string, string[]> = {
    page_view: ['dl'],
    view_item: [],
    view_item_list: [],
    add_to_cart: [],
    remove_from_cart: [],
    purchase: [],
    begin_checkout: [],
    default: []
};

export type DuplicateCheckMode = 'name-only' | 'name-and-id' | 'disabled';

export type TrackAnalyticsOptions = {
    duplicateCheckMode?: DuplicateCheckMode;
    validateStructure?: boolean;
    ignorePreNavigationEvents?: boolean;
};

export function trackAnalytics(
    page: Page,
    options: TrackAnalyticsOptions = {}
) {
    const {
        duplicateCheckMode = 'name-and-id',
        validateStructure = true,
        ignorePreNavigationEvents = true
    } = options;

    const events: GaEvent[] = [];
    const malformedEvents: { event: GaEvent; missingParams: string[] }[] = [];
    let navigationStarted = false;

    const handler = (request: Request) => {
        if (!request.url().startsWith(siteConfig.analytics.requestUrl)) {
            return;
        }

        if (ignorePreNavigationEvents && !navigationStarted) {
            return;
        }

        const params = getParams(request);
        const name =
            params.get(siteConfig.analytics.eventNameParam) ?? 'unknown';
        const id = params.get(siteConfig.analytics.eventIdParam) ?? undefined;

        const event: GaEvent = {
            name,
            id,
            requestUrl: request.url(),
            params
        };

        events.push(event);

        if (validateStructure) {
            const missingParams = validateEventStructure(event);
            if (missingParams.length > 0) {
                malformedEvents.push({ event, missingParams });
            }
        }
    };

    const frameNavigatedHandler = () => {
        navigationStarted = true;
    };

    page.on('request', handler);
    page.on('framenavigated', frameNavigatedHandler);

    const getDuplicateKey = (event: GaEvent): string => {
        switch (duplicateCheckMode) {
            case 'name-only':
                return event.name;
            case 'name-and-id':
                return `${event.name}:${event.id ?? ''}`;
            case 'disabled':
                return `${event.name}:${event.id ?? ''}:${Date.now()}:${Math.random()}`;
        }
    };

    const expectNoDuplicates = () => {
        if (duplicateCheckMode === 'disabled') {
            return;
        }

        const seen = new Map<string, number>();
        for (const event of events) {
            const key = getDuplicateKey(event);
            seen.set(key, (seen.get(key) ?? 0) + 1);
        }
        const duplicates = [...seen.entries()].filter(([, count]) => count > 1);
        if (duplicates.length) {
            const modeDescription =
                duplicateCheckMode === 'name-only'
                    ? 'by event name'
                    : 'by event name and ID';
            throw new Error(
                `Duplicate GA events detected (${modeDescription}): ${duplicates.map(([key]) => key).join(', ')}`
            );
        }
    };

    const expectNoMalformedEvents = () => {
        if (malformedEvents.length > 0) {
            const details = malformedEvents
                .map(
                    ({ event, missingParams }) =>
                        `Event "${event.name}" missing: ${missingParams.join(', ')}`
                )
                .join('\n');
            throw new Error(`Malformed GA events detected:\n${details}`);
        }
    };

    return {
        events,
        malformedEvents,

        startTracking: () => {
            navigationStarted = true;
        },

        cleanup: () => {
            page.off('request', handler);
            page.off('framenavigated', frameNavigatedHandler);
        },

        expectEvents: async (expectedNames: string[], timeout = 15_000) => {
            await expect
                .poll(() => events.map(event => event.name), {
                    timeout,
                    message: 'Waiting for GA events'
                })
                .toEqual(expect.arrayContaining(expectedNames));
            expectNoDuplicates();
            if (validateStructure) {
                expectNoMalformedEvents();
            }
        },

        expectValidEvents: async (
            expectedNames: string[],
            timeout = 15_000
        ) => {
            await expect
                .poll(() => events.map(event => event.name), {
                    timeout,
                    message: 'Waiting for GA events'
                })
                .toEqual(expect.arrayContaining(expectedNames));
            expectNoDuplicates();
            expectNoMalformedEvents();
        }
    };
}

function validateEventStructure(event: GaEvent): string[] {
    const requiredParams =
        requiredEventParams[event.name] ?? requiredEventParams['default'];
    const missingParams: string[] = [];

    for (const param of requiredParams) {
        if (!event.params.has(param) && !hasParamPrefix(event.params, param)) {
            missingParams.push(param);
        }
    }

    return missingParams;
}

function hasParamPrefix(params: URLSearchParams, prefix: string): boolean {
    for (const key of params.keys()) {
        if (key.startsWith(prefix)) {
            return true;
        }
    }
    return false;
}

function getParams(request: Request) {
    const url = new URL(request.url());
    const params = new URLSearchParams(url.searchParams);
    const postData = request.postData();

    if (postData) {
        const postParams = new URLSearchParams(postData);
        postParams.forEach((value, key) => {
            if (!params.has(key)) {
                params.set(key, value);
            }
        });
    }

    return params;
}
