import { app, ipcMain, powerMonitor } from 'electron';
import { access, rm } from 'fs/promises';
import uniq from 'lodash/uniq';
import MpvAPI from 'node-mpv';
import { pid } from 'node:process';
import process from 'process';

import { getMainWindow, sendToastToRenderer } from '../../../index';
import log from '../../../logger';
import { store } from '../settings';

import { isMacOS, isWindows } from '/@/main/env';
import { PlayerData } from '/@/shared/types/domain-types';

declare module 'node-mpv';

// function wait(timeout: number) {
//     return new Promise((resolve) => {
//         setTimeout(() => {
//             resolve('resolved');
//         }, timeout);
//     });
// }

let mpvInstance: MpvAPI | null = null;
let currentPlayerData: null | PlayerData = null;
const socketPath = isWindows() ? `\\\\.\\pipe\\mpvserver-${pid}` : `/tmp/node-mpv-${pid}.sock`;

const NodeMpvErrorCode = {
    0: 'Unable to load file or stream',
    1: 'Invalid argument',
    2: 'Binary not found',
    3: 'IPC command invalid',
    4: 'Unable to bind IPC socket',
    5: 'Connection timeout',
    6: 'MPV is already running',
    7: 'Could not send IPC message',
    8: 'MPV is not running',
    9: 'Unsupported protocol',
};

type NodeMpvError = {
    errcode: number;
    method: string;
    stackTrace: string;
    verbose: string;
};

const mpvLog = (
    data: {
        action: string;
        level?: 'debug' | 'error' | 'info' | 'warn';
        toast?: 'info' | 'success' | 'warning';
    },
    err?: NodeMpvError,
) => {
    const { action, toast } = data;

    if (err) {
        const message = `${action} - mpv errorcode ${err.errcode} - ${
            NodeMpvErrorCode[err.errcode as keyof typeof NodeMpvErrorCode]
        }`;

        sendToastToRenderer({ message, type: 'error' });
        log.error(message);
        return;
    }

    const level = data.level ?? 'info';
    log[level](action);
    if (toast) {
        sendToastToRenderer({ message: action, type: toast });
    }
};

const MPV_BINARY_PATH = store.get('mpv_path') as string | undefined;
const MACOS_MPV_BINARY_PATHS = ['/opt/homebrew/bin/mpv', '/usr/local/bin/mpv'];

const prefetchPlaylistParams = [
    '--prefetch-playlist=no',
    '--prefetch-playlist=yes',
    '--prefetch-playlist',
];

const DEFAULT_MPV_PARAMETERS = (extraParameters?: string[]) => {
    const parameters = ['--idle=yes', '--no-config', '--load-scripts=no'];

    if (!extraParameters?.some((param) => prefetchPlaylistParams.includes(param))) {
        parameters.push('--prefetch-playlist=yes');
    }

    // Without these, mpv/ffmpeg will block indefinitely on a dead TCP connection
    // instead of failing or reconnecting. This commonly happens when the OS network
    // adapter resets after the system wakes from sleep while a stream is open.
    if (!extraParameters?.some((param) => param.startsWith('--network-timeout'))) {
        parameters.push('--network-timeout=10');
    }

    if (!extraParameters?.some((param) => param.startsWith('--stream-lavf-o'))) {
        parameters.push(
            '--stream-lavf-o=reconnect=1,reconnect_streamed=1,reconnect_at_eof=1,reconnect_delay_max=5',
        );
    }

    return parameters;
};

const resolveMpvBinaryPath = async (binaryPath?: string) => {
    if (binaryPath) {
        return binaryPath;
    }

    if (MPV_BINARY_PATH) {
        return MPV_BINARY_PATH;
    }

    if (!isMacOS()) {
        return undefined;
    }

    for (const candidate of MACOS_MPV_BINARY_PATHS) {
        try {
            await access(candidate);
            return candidate;
        } catch {
            // Try the next common Homebrew location.
        }
    }

    return undefined;
};

const createMpv = async (data: {
    binaryPath?: string;
    extraParameters?: string[];
    properties?: Record<string, any>;
}): Promise<MpvAPI> => {
    const { binaryPath, extraParameters, properties } = data;
    const resolvedBinaryPath = await resolveMpvBinaryPath(binaryPath);
    const normalizedExtraParameters = (extraParameters ?? [])
        .map((param) => param.trim())
        .filter((param) => param.length > 0);

    const params = uniq([
        ...DEFAULT_MPV_PARAMETERS(normalizedExtraParameters),
        ...normalizedExtraParameters,
    ]);

    const mpv = new MpvAPI(
        {
            audio_only: true,
            auto_restart: false,
            binary: resolvedBinaryPath,
            socket: socketPath,
            time_update: 1,
        },
        params,
    );

    try {
        await mpv.start();
        log.info('mpv initialized', { binary: resolvedBinaryPath ?? 'bundled/default' });
    } catch (error: any) {
        log.error('mpv failed to start', error);
    } finally {
        await mpv.setMultipleProperties(properties || {});
    }

    let previousPlaylistPos: number | undefined;

    mpv.on('status', (status) => {
        if (status.property === 'playlist-pos') {
            const currentPos = typeof status.value === 'number' ? status.value : undefined;

            // mpv uses playlist-pos = -1 when nothing is playing (ended, cleared, load failure, etc).
            if (currentPos === -1) {
                if (previousPlaylistPos === 0) {
                    getMainWindow()?.webContents.send('renderer-player-track-ended');
                }
                mpv?.pause();
                previousPlaylistPos = currentPos;
                return;
            }

            // In our 2-item queue model, playlist-pos should normally be 0.
            // When mpv auto-advances to the next track it becomes > 0 (typically 1).
            if (typeof currentPos === 'number' && currentPos > 0) {
                getMainWindow()?.webContents.send('renderer-player-auto-next');
            }

            previousPlaylistPos = currentPos;
        }
    });

    // Automatically updates the play button when the player is playing
    mpv.on('resumed', () => {
        getMainWindow()?.webContents.send('renderer-player-play');
    });

    // Automatically updates the play button when the player is stopped
    mpv.on('stopped', () => {
        getMainWindow()?.webContents.send('renderer-player-stop');
    });

    // Automatically updates the play button when the player is paused
    mpv.on('paused', () => {
        getMainWindow()?.webContents.send('renderer-player-pause');
    });

    // Event output every interval set by time_update, used to update the current time
    mpv.on('timeposition', (time: number) => {
        getMainWindow()?.webContents.send('renderer-player-current-time', time);
    });

    return mpv;
};

export const getMpvInstance = () => {
    return mpvInstance;
};

const QUIT_TIMEOUT_MS = 3000;

const killMpvProcess = (mpv: MpvAPI) => {
    const mpvProcess = (mpv as any).process || (mpv as any).mpvProcess;
    if (mpvProcess && typeof mpvProcess.kill === 'function') {
        try {
            mpvProcess.kill('SIGTERM');
        } catch (killErr) {
            mpvLog({ action: 'Failed to kill mpv process' }, killErr as NodeMpvError);
        }
    }
};

const quit = async (instance?: MpvAPI | null) => {
    const mpv = instance || getMpvInstance();
    if (mpv) {
        try {
            // mpv.quit() resolves only when mpv replies over IPC. If mpv's command queue
            // is wedged (e.g. blocked on a dead network stream after the system resumes
            // from sleep), that reply never arrives, so this must not be allowed to hang
            // forever - fall back to killing the process directly.
            let timedOut = false;
            await Promise.race([
                mpv.quit(),
                new Promise((resolve) => {
                    setTimeout(() => {
                        timedOut = true;
                        resolve(undefined);
                    }, QUIT_TIMEOUT_MS);
                }),
            ]);

            if (timedOut) {
                killMpvProcess(mpv);
            }
        } catch {
            // If quit() fails, try to kill the process directly
            killMpvProcess(mpv);
        }
        if (!isWindows()) {
            try {
                await rm(socketPath);
            } catch {
                // Ignore errors when removing socket file
            }
        }
    }
};

const setAudioPlayerFallback = (isError: boolean) => {
    if (isError) {
        log.warn('Falling back to web player');
    }
    getMainWindow()?.webContents.send('renderer-player-fallback', isError);
};

ipcMain.on('player-set-properties', async (_event, data: Record<string, any>) => {
    mpvLog({ action: `Setting properties: ${JSON.stringify(data)}`, level: 'debug' });
    if (data.length === 0) {
        return;
    }

    try {
        if (data.length === 1) {
            getMpvInstance()?.setProperty(Object.keys(data)[0], Object.values(data)[0]);
        } else {
            getMpvInstance()?.setMultipleProperties(data);
        }
    } catch (err: any | NodeMpvError) {
        mpvLog({ action: `Failed to set properties: ${JSON.stringify(data)}` }, err);
    }
});

ipcMain.handle(
    'player-restart',
    async (_event, data: { extraParameters?: string[]; properties?: Record<string, any> }) => {
        try {
            mpvLog({
                action: `Attempting to initialize mpv with parameters: ${JSON.stringify(data)}`,
                level: 'debug',
            });

            // Clean up previous mpv instance
            getMpvInstance()?.stop();
            getMpvInstance()
                ?.quit()
                .catch((error) => {
                    mpvLog({ action: 'Failed to quit existing MPV' }, error);
                });
            mpvInstance = null;

            mpvInstance = await createMpv(data);
            mpvLog({ action: 'Restarted mpv', toast: 'success' });
            setAudioPlayerFallback(false);
        } catch (err: any | NodeMpvError) {
            mpvLog({ action: 'Failed to restart mpv, falling back to web player' }, err);
            setAudioPlayerFallback(true);
        }
    },
);

ipcMain.handle(
    'player-initialize',
    async (_event, data: { extraParameters?: string[]; properties?: Record<string, any> }) => {
        try {
            mpvLog({
                action: `Attempting to initialize mpv with parameters: ${JSON.stringify(data)}`,
                level: 'debug',
            });
            mpvInstance = await createMpv(data);
            setAudioPlayerFallback(false);
        } catch (err: any | NodeMpvError) {
            mpvLog({ action: 'Failed to initialize mpv, falling back to web player' }, err);
            setAudioPlayerFallback(true);
        }
    },
);

ipcMain.on('player-quit', async () => {
    try {
        await getMpvInstance()?.stop();
        await quit();
    } catch (err: any | NodeMpvError) {
        mpvLog({ action: 'Failed to quit mpv' }, err);
    } finally {
        mpvInstance = null;
    }
});

ipcMain.handle('player-is-running', async () => {
    return getMpvInstance()?.isRunning();
});

ipcMain.handle('player-clean-up', async () => {
    getMpvInstance()?.stop();
    getMpvInstance()?.clearPlaylist();
});

ipcMain.on('player-start', async () => {
    try {
        await getMpvInstance()?.play();
    } catch (err: any | NodeMpvError) {
        mpvLog({ action: 'Failed to start mpv playback' }, err);
    }
});

// Starts the player
ipcMain.on('player-play', async () => {
    try {
        await getMpvInstance()?.play();
    } catch (err: any | NodeMpvError) {
        mpvLog({ action: 'Failed to start mpv playback' }, err);
    }
});

// Pauses the player
ipcMain.on('player-pause', async () => {
    try {
        await getMpvInstance()?.pause();
    } catch (err: any | NodeMpvError) {
        mpvLog({ action: 'Failed to pause mpv playback' }, err);
    }
});

// Stops the player
ipcMain.on('player-stop', async () => {
    try {
        await getMpvInstance()?.stop();
    } catch (err: any | NodeMpvError) {
        mpvLog({ action: 'Failed to stop mpv playback' }, err);
    }
});

// Goes to the next track in the playlist
ipcMain.on('player-next', async () => {
    try {
        await getMpvInstance()?.next();
    } catch (err: any | NodeMpvError) {
        mpvLog({ action: 'Failed to go to next track' }, err);
    }
});

// Goes to the previous track in the playlist
ipcMain.on('player-previous', async () => {
    try {
        await getMpvInstance()?.prev();
    } catch (err: any | NodeMpvError) {
        mpvLog({ action: 'Failed to go to previous track' }, err);
    }
});

// Seeks forward or backward by the given amount of seconds
ipcMain.on('player-seek', async (_event, time: number) => {
    try {
        await getMpvInstance()?.seek(time);
    } catch (err: any | NodeMpvError) {
        mpvLog({ action: `Failed to seek by ${time} seconds` }, err);
    }
});

// Seeks to the given time in seconds
ipcMain.on('player-seek-to', async (_event, time: number) => {
    try {
        await getMpvInstance()?.goToPosition(time);
    } catch (err: any | NodeMpvError) {
        mpvLog({ action: `Failed to seek to ${time} seconds` }, err);
    }
});

// Sets the queue in position 0 and 1 to the given data. Used when manually starting a song or using the next/prev buttons
ipcMain.on('player-set-queue', async (_event, current?: string, next?: string, pause?: boolean) => {
    if (!current && !next) {
        try {
            await getMpvInstance()?.clearPlaylist();
            await getMpvInstance()?.pause();
            return;
        } catch (err: any | NodeMpvError) {
            mpvLog({ action: `Failed to clear play queue` }, err);
        }
    }

    try {
        if (current) {
            try {
                await getMpvInstance()?.load(current, 'replace');
            } catch (error: any | NodeMpvError) {
                mpvLog({ action: `Failed to load current song` }, error);
                await getMpvInstance()?.play();
            }

            if (next) {
                await getMpvInstance()?.load(next, 'append');
            }
        }

        if (pause) {
            await getMpvInstance()?.pause();
        } else if (pause === false) {
            // Only force play if pause is explicitly false
            await getMpvInstance()?.play();
        }
    } catch (err: any | NodeMpvError) {
        mpvLog({ action: `Failed to set play queue` }, err);
    }
});

// Replaces the queue in position 1 to the given data
ipcMain.on('player-set-queue-next', async (_event, url?: string) => {
    try {
        const size = await getMpvInstance()?.getPlaylistSize();

        if (size && size > 1) {
            await getMpvInstance()?.playlistRemove(1);
        }

        if (url) {
            getMpvInstance()?.load(url, 'append');
        }
    } catch (err: any | NodeMpvError) {
        mpvLog({ action: `Failed to set play queue` }, err);
    }
});

// Sets the next song in the queue when reaching the end of the queue
ipcMain.on('player-auto-next', async (_event, url?: string) => {
    // Always keep the current song as position 0 in the mpv queue
    // This allows us to easily set update the next song in the queue without
    // disturbing the currently playing song

    try {
        await getMpvInstance()
            ?.playlistRemove(0)
            .catch(() => {
                getMpvInstance()?.pause();
            });

        if (url) {
            await getMpvInstance()?.load(url, 'append');
        }
    } catch (err: any | NodeMpvError) {
        mpvLog({ action: `Failed to load next song` }, err);
    }
});

// Sets the volume to the given value (0-100)
ipcMain.on('player-volume', async (_event, value: number) => {
    try {
        if (!value || value < 0 || value > 100) {
            return;
        }

        await getMpvInstance()?.volume(value);
    } catch (err: any | NodeMpvError) {
        mpvLog({ action: `Failed to set volume to ${value}` }, err);
    }
});

// Toggles the mute status
ipcMain.on('player-mute', async (_event, mute: boolean) => {
    try {
        await getMpvInstance()?.mute(mute);
    } catch (err: any | NodeMpvError) {
        mpvLog({ action: `Failed to set mute status` }, err);
    }
});

ipcMain.handle('player-get-time', async (): Promise<number | undefined> => {
    try {
        const mpv = getMpvInstance();
        if (!mpv) {
            return undefined;
        }
        return await mpv.getTimePosition();
    } catch (err: any | NodeMpvError) {
        // Err 3: IPC command invalid — e.g. time-pos unavailable when idle / between tracks
        if (err?.errcode === 3) {
            return undefined;
        }
        mpvLog({ action: `Failed to get current time` }, err);
        return undefined;
    }
});

// Updates the current player metadata (song data)
ipcMain.on('player-update-metadata', (_event, data: PlayerData) => {
    currentPlayerData = data;
});

// Returns the current player metadata (song data)
ipcMain.handle('player-metadata', async (): Promise<null | PlayerData> => {
    return currentPlayerData;
});

// Returns the stream metadata from mpv (for radio streams)
ipcMain.handle(
    'player-stream-metadata',
    async (): Promise<null | { artist: null | string; title: null | string }> => {
        try {
            const metadata = await getMpvInstance()?.getProperty('metadata');
            if (metadata && typeof metadata === 'object') {
                // Try to get separate title and artist fields first
                let artist: null | string =
                    (metadata['artist'] as string) ||
                    (metadata['ARTIST'] as string) ||
                    (metadata['icy-artist'] as string) ||
                    null;
                let title: null | string =
                    (metadata['title'] as string) || (metadata['TITLE'] as string) || null;

                // If we don't have separate fields, try to parse from combined formats
                if (!title && !artist) {
                    const combinedTitle =
                        (metadata['icy-title'] as string) ||
                        (metadata['StreamTitle'] as string) ||
                        (metadata['stream-title'] as string) ||
                        null;

                    if (combinedTitle && typeof combinedTitle === 'string') {
                        // Try to parse "Artist - Title" format
                        const match = combinedTitle.match(/^(.*?)\s*[-–—]\s*(.+)$/);
                        if (match) {
                            artist = match[1].trim() || null;
                            title = match[2].trim() || null;
                        } else {
                            // If no separator found, treat the whole thing as title
                            title = combinedTitle;
                        }
                    }
                } else if (!title) {
                    // If we have artist but no title, try to get from combined format
                    const combinedTitle =
                        (metadata['icy-title'] as string) ||
                        (metadata['StreamTitle'] as string) ||
                        (metadata['stream-title'] as string) ||
                        null;
                    if (combinedTitle && typeof combinedTitle === 'string') {
                        title = combinedTitle;
                    }
                } else if (!artist) {
                    // If we have title but no artist, try to get from combined format
                    const combinedTitle =
                        (metadata['icy-title'] as string) ||
                        (metadata['StreamTitle'] as string) ||
                        (metadata['stream-title'] as string) ||
                        null;
                    if (
                        combinedTitle &&
                        typeof combinedTitle === 'string' &&
                        combinedTitle !== title
                    ) {
                        // Try to parse artist from combined format
                        const match = combinedTitle.match(/^(.*?)\s*[-–—]\s*(.+)$/);
                        if (match && match[2].trim() === title) {
                            artist = match[1].trim() || null;
                        }
                    }
                }

                return { artist, title };
            }
            return null;
        } catch (err: any | NodeMpvError) {
            mpvLog({ action: `Failed to get stream metadata` }, err);
            return null;
        }
    },
);

ipcMain.handle(
    'player-get-audio-devices',
    async (): Promise<{ label: string; value: string }[]> => {
        try {
            const instance = getMpvInstance();
            let tempInstance: MpvAPI | null = null;
            let mpvToUse: MpvAPI | null = null;

            if (instance && instance.isRunning()) {
                mpvToUse = instance;
            } else {
                try {
                    tempInstance = await createMpv({});
                    mpvToUse = tempInstance;
                } catch (err: any | NodeMpvError) {
                    mpvLog(
                        { action: 'Failed to create temporary MPV instance for audio device list' },
                        err,
                    );
                    return [];
                }
            }

            try {
                const deviceList = await mpvToUse.getProperty('audio-device-list');

                if (!deviceList || !Array.isArray(deviceList)) {
                    return [];
                }

                const devices = deviceList.map((device: any) => {
                    const name = device.name || device.description || 'Unknown Device';
                    const description = device.description || '';
                    const label = description ? `${name} (${description})` : name;
                    return {
                        label,
                        value: name,
                    };
                });

                return devices;
            } finally {
                if (tempInstance && tempInstance !== instance) {
                    try {
                        await quit(tempInstance);
                    } catch {
                        // Ignore
                    }
                }
            }
        } catch (err: any | NodeMpvError) {
            mpvLog({ action: 'Failed to get audio devices' }, err);
            return [];
        }
    },
);

enum MpvState {
    STARTED,
    IN_PROGRESS,
    DONE,
}

let mpvState = MpvState.STARTED;

// Cleanup function that can be called from multiple places
const cleanupMpv = async (force = false) => {
    if (mpvState === MpvState.DONE && !force) {
        return;
    }

    const instance = getMpvInstance();
    if (instance) {
        try {
            if (!force) {
                await instance.stop();
            }
            await quit(instance);
        } catch (err: any | NodeMpvError) {
            mpvLog({ action: `Failed to cleanup mpv` }, err);
            // Force kill as fallback
            const mpvProcess = (instance as any).process || (instance as any).mpvProcess;
            if (mpvProcess && typeof mpvProcess.kill === 'function') {
                try {
                    mpvProcess.kill('SIGKILL');
                } catch {
                    // Ignore kill errors
                }
            }
        } finally {
            mpvInstance = null;
        }
    }
};

// When the OS resumes from sleep, any network stream mpv had open is likely dead
// (the connection silently dropped while the network adapter was suspended). Tell
// the renderer to reload mpv so it reconnects with a fresh stream instead of staying
// stuck on the old, now-dead connection until the app is manually restarted.
powerMonitor.on('resume', () => {
    if (getMpvInstance()) {
        mpvLog({ action: 'System resumed from sleep, reloading mpv' });
        getMainWindow()?.webContents.send('renderer-mpv-reconnect');
    }
});

app.on('before-quit', async (event) => {
    switch (mpvState) {
        case MpvState.DONE:
            return;
        case MpvState.IN_PROGRESS:
            event.preventDefault();
            break;
        case MpvState.STARTED: {
            try {
                mpvState = MpvState.IN_PROGRESS;
                event.preventDefault();
                await cleanupMpv();
            } catch (err: any | NodeMpvError) {
                mpvLog({ action: `Failed to cleanly before-quit` }, err);
            } finally {
                mpvState = MpvState.DONE;
                app.quit();
            }
            break;
        }
    }
});

// Handle process exit events to ensure mpv is killed even if app crashes
process.on('exit', () => {
    const instance = getMpvInstance();
    if (instance) {
        // Try to access and kill the process directly
        const mpvProcess = (instance as any).process || (instance as any).mpvProcess;
        if (mpvProcess && typeof mpvProcess.kill === 'function') {
            try {
                mpvProcess.kill('SIGKILL');
            } catch {
                // Ignore errors during exit
            }
        }
    }
});

// Handle signals that can terminate the process
process.on('SIGINT', async () => {
    await cleanupMpv(true);
    process.exit(0);
});

process.on('SIGTERM', async () => {
    await cleanupMpv(true);
    process.exit(0);
});

// Handle uncaught exceptions - cleanup mpv before crashing
process.on('uncaughtException', async (error) => {
    log.error('Uncaught exception:', error);
    await cleanupMpv(true).catch(() => {
        // Ignore cleanup errors during crash
    });
});

// Handle unhandled rejections - cleanup mpv
process.on('unhandledRejection', async (reason) => {
    log.error('Unhandled rejection:', reason);
    await cleanupMpv(true).catch(() => {
        // Ignore cleanup errors
    });
});
