import { spawn } from 'node:child_process';
import path from 'node:path';
import * as fs from 'fs';
import { shell } from 'electron';
import { AppEnv, waitAppEnvReady } from '../env';
import { Log } from '../log';
import { getFFmpegExecutablePath, getFFprobeExecutablePath, getPythonExecutablePath, resourceExists, isDevelopment } from '../../lib/resource-path';
import { normalizePath, normalizeOutputPath } from '../../lib/path-util';
import { getMacOSPythonPath } from '../../lib/python-setup';

const isWin = process.platform === 'win32';
const isMac = process.platform === 'darwin';

// Python路径缓存，避免重复验证
let cachedPythonPath: string | null = null;

const ensureWritableDirectory = (candidates: Array<string | null | undefined>): string => {
    for (const candidate of candidates) {
        if (!candidate || typeof candidate !== 'string') {
            continue;
        }

        try {
            fs.mkdirSync(candidate, { recursive: true });
            return candidate;
        } catch (error) {
            Log.warn('ensureWritableDirectory: failed to create directory', {
                path: candidate,
                error: (error as Error).message,
            });
        }
    }

    const fallback = process.env.TEMP || process.env.TMP || process.cwd();
    fs.mkdirSync(fallback, { recursive: true });
    return fallback;
};

export const getRuntimeTempRoot = (): string => {
    const appRoot = AppEnv.appRoot || process.env.APP_ROOT || process.cwd();
    return ensureWritableDirectory([
        AppEnv.tempRoot,
        AppEnv.dataRoot ? path.join(AppEnv.dataRoot, 'temp') : null,
        AppEnv.userData ? path.join(AppEnv.userData, 'temp') : null,
        appRoot ? path.join(appRoot, 'data', 'temp') : null,
        path.join(process.cwd(), 'data', 'temp'),
    ]);
};

export const getRuntimeFontconfigCacheDir = (): string => {
    return ensureWritableDirectory([
        path.join(getRuntimeTempRoot(), 'fontconfig-cache'),
        AppEnv.userData ? path.join(AppEnv.userData, 'fontconfig-cache') : null,
        path.join(process.cwd(), 'data', 'temp', 'fontconfig-cache'),
    ]);
};

export const buildRuntimeProcessEnv = (extraEnv?: Record<string, string | undefined>): NodeJS.ProcessEnv => {
    const tempRoot = getRuntimeTempRoot();
    const fontCacheDir = getRuntimeFontconfigCacheDir();

    return {
        ...process.env,
        TEMP: tempRoot,
        TMP: tempRoot,
        TMPDIR: tempRoot,
        FONTCONFIG_FILE: '',
        FONTCONFIG_PATH: '',
        FONTCONFIG_CACHE: fontCacheDir,
        XDG_CACHE_HOME: tempRoot,
        ...(extraEnv || {}),
    };
};

/**
 * 动态检测 Python 可执行文件路径
 * 优先级：系统 PATH 中的 python > py > python3
 * @returns Promise<string> Python 可执行文件路径
 */
export const getPythonPath = async (): Promise<string> => {
    // 如果已经缓存了有效路径，直接返回
    if (cachedPythonPath) {
        Log.debug('getPythonPath: 使用缓存路径', { path: cachedPythonPath });
        return cachedPythonPath;
    }

    Log.info('getPythonPath: 开始检测 Python 路径');

    if (!isWin) {
        const standalonePython = getMacOSPythonPath();
        if (standalonePython) {
            Log.info('getPythonPath: ✅ 使用独立 Python', { path: standalonePython });
            cachedPythonPath = standalonePython;
            return standalonePython;
        }
        throw new Error('macOS Python 环境尚未初始化，请等待应用完成初始化');
    }

    await waitAppEnvReady();
    const bundledPythonPath = getPythonExecutablePath();
    if (resourceExists(bundledPythonPath)) {
        try {
            Log.info('getPythonPath: trying bundled Python', { path: bundledPythonPath });
            const isAvailable = await verifyPythonExecutable(bundledPythonPath);
            if (isAvailable) {
                Log.info('getPythonPath: using bundled Python', { path: bundledPythonPath });
                cachedPythonPath = bundledPythonPath;
                return bundledPythonPath;
            }
        } catch (error) {
            Log.warn('getPythonPath: bundled Python unavailable', {
                path: bundledPythonPath,
                error: (error as Error).message,
            });
        }
    }

    const pythonCommands = [];
    pythonCommands.push('py', 'python', 'python3');

    // 逐个测试 Python 命令是否可用
    for (const command of pythonCommands) {
        try {
            Log.info('getPythonPath: 尝试命令', { command });

            const isAvailable = await verifyPythonExecutable(command);
            if (isAvailable) {
                Log.info('getPythonPath: ✅ 找到可用的 Python', { command });
                cachedPythonPath = command;
                return command;
            }
        } catch (error) {
            Log.warn('getPythonPath: 命令不可用', { command, error: (error as Error).message });
        }
    }

    // 所有尝试都失败
    const errorMsg = `❌ 无法找到可用的 Python：\n` +
                     `尝试的命令: ${pythonCommands.join(', ')}\n` +
                     `请安装 Python 或确保其在系统 PATH 中。`;
    Log.error('getPythonPath', errorMsg);
    throw new Error(errorMsg);
};

/**
 * 验证 Python 命令是否可执行
 * @param pythonCommand Python 命令（python, py, python3 等）
 * @returns Promise<boolean> 是否可执行
 */
const verifyPythonExecutable = async (pythonCommand: string): Promise<boolean> => {
    return new Promise((resolve) => {
        try {
            Log.info('verifyPythonExecutable: 开始验证', { pythonCommand });

            const child = spawn(pythonCommand, ['--version'], {
                shell: false,
                stdio: ['pipe', 'pipe', 'pipe'],
            });

            let hasOutput = false;
            let isResolved = false;

            const cleanup = () => {
                if (!isResolved) {
                    isResolved = true;
                    try {
                        child.kill('SIGTERM');
                    } catch (e) {}
                }
            };

            const timeout = setTimeout(() => {
                Log.warn('verifyPythonExecutable: 超时', { pythonCommand });
                cleanup();
                resolve(false);
            }, 3000); // 3秒超时

            child.stdout?.on('data', () => {
                hasOutput = true;
                Log.debug('verifyPythonExecutable: 获得输出', { pythonCommand });
            });

            child.stderr?.on('data', () => {
                hasOutput = true;
                Log.debug('verifyPythonExecutable: 获得错误输出', { pythonCommand });
            });

            child.on('close', (code) => {
                clearTimeout(timeout);
                if (!isResolved) {
                    isResolved = true;
                    const success = code === 0 && hasOutput;
                    Log.info('verifyPythonExecutable: 完成', { pythonCommand, code, hasOutput, success });
                    resolve(success);
                }
            });

            child.on('error', (error) => {
                clearTimeout(timeout);
                if (!isResolved) {
                    isResolved = true;
                    Log.warn('verifyPythonExecutable: 错误', { pythonCommand, error: error.message });
                    cleanup();
                    resolve(false);
                }
            });
        } catch (error: any) {
            Log.error('verifyPythonExecutable: 异常', { pythonCommand, error: error.message });
            resolve(false);
        }
    });
};

// FFmpeg路径缓存，避免重复验证
let cachedFFmpegPath: string | null = null;
const invalidFFmpegPaths = new Set<string>();
const invalidFFprobePaths = new Set<string>();
const ILLEGAL_INSTRUCTION_EXIT_CODES = new Set([3221225501]);
const FFMPEG_RUNTIME_VERIFY_ARGS = [
    '-hide_banner',
    '-loglevel', 'error',
    '-f', 'lavfi',
    '-i', 'color=size=16x16:rate=1:color=black',
    '-frames:v', '1',
    '-c:v', 'libx264',
    '-pix_fmt', 'yuv420p',
    '-f', 'null',
    '-'
];

const getBundledExecutableCandidates = (primaryPath: string): string[] => {
    const binaryName = path.basename(primaryPath);
    const candidates = [
        primaryPath,
        AppEnv.resourceBundleRoot ? path.join(AppEnv.resourceBundleRoot, 'ffmpeg', binaryName) : null,
        process.resourcesPath ? path.join(process.resourcesPath, 'resources-bundles', 'ffmpeg', binaryName) : null,
        process.resourcesPath ? path.join(path.dirname(process.resourcesPath), 'resources-bundles', 'ffmpeg', binaryName) : null,
    ];

    if (isWin) {
        const parentDir = path.dirname(primaryPath);
        const backupDir = `${parentDir}-backup`;
        const backupPath = path.join(backupDir, path.basename(primaryPath));
        candidates.push(backupPath);
    }

    return Array.from(new Set(candidates.filter(Boolean)));
};

const markExecutableAsInvalid = (
    invalidSet: Set<string>,
    executablePath: string,
    tag: 'FFmpeg' | 'FFprobe',
    reason: string
) => {
    if (!executablePath || invalidSet.has(executablePath)) {
        return;
    }

    invalidSet.add(executablePath);
    Log.warn(`${tag}: invalid executable path`, {
        executablePath,
        reason,
    });
};

const verifyRuntimeExecutable = async (
    executablePath: string,
    args: string[],
    tag: 'FFmpeg' | 'FFprobe',
    options: {
        requireOutput?: boolean;
    } = {}
): Promise<boolean> => {
    return new Promise((resolve) => {
        try {
            const child = spawn(executablePath, args, {
                shell: false,
                stdio: ['pipe', 'pipe', 'pipe'],
            });

            let hasOutput = false;
            let settled = false;
            const requireOutput = options.requireOutput ?? true;

            const finish = (result: boolean) => {
                if (!settled) {
                    settled = true;
                    resolve(result);
                }
            };

            const timer = setTimeout(() => {
                try {
                    child.kill('SIGTERM');
                } catch (e) {}
                Log.warn(`verify${tag}Executable: timeout`, { executablePath, args });
                finish(false);
            }, 4000);

            child.stdout?.on('data', () => {
                hasOutput = true;
            });

            child.stderr?.on('data', () => {
                hasOutput = true;
            });

            child.on('close', (code) => {
                clearTimeout(timer);
                const success = code === 0 && (!requireOutput || hasOutput);
                Log.info(`verify${tag}Executable: complete`, {
                    executablePath,
                    args,
                    code,
                    hasOutput,
                    requireOutput,
                    success,
                });
                finish(success);
            });

            child.on('error', (error) => {
                clearTimeout(timer);
                Log.warn(`verify${tag}Executable: error`, { executablePath, args, error: error.message });
                finish(false);
            });
        } catch (error: any) {
            Log.error(`verify${tag}Executable: exception`, { executablePath, args, error: error.message });
            resolve(false);
        }
    });
};

const verifyBundledFFmpegCandidate = async (ffmpegPath: string): Promise<boolean> => {
    return verifyRuntimeExecutable(ffmpegPath, FFMPEG_RUNTIME_VERIFY_ARGS, 'FFmpeg', {
        requireOutput: false,
    });
};

const verifyBundledFFprobeCandidate = async (ffprobePath: string): Promise<boolean> => {
    return verifyRuntimeExecutable(ffprobePath, ['-version'], 'FFprobe');
};

const findFallbackFFmpegPath = async (excludePaths: string[]): Promise<string | null> => {
    const excluded = new Set(excludePaths);
    const bundledCandidates = getBundledExecutableCandidates(getFFmpegExecutablePath());

    for (const candidate of bundledCandidates) {
        if (!candidate || excluded.has(candidate) || invalidFFmpegPaths.has(candidate) || !resourceExists(candidate)) {
            continue;
        }

        if (await verifyBundledFFmpegCandidate(candidate)) {
            return candidate;
        }

        markExecutableAsInvalid(invalidFFmpegPaths, candidate, 'FFmpeg', 'fallback verification failed');
    }

    if (isDevelopment()) {
        const systemFFmpegPath = isWin ? 'ffmpeg.exe' : 'ffmpeg';
        if (!excluded.has(systemFFmpegPath) && !invalidFFmpegPaths.has(systemFFmpegPath)) {
            if (await verifyFFmpegExecutable(systemFFmpegPath)) {
                return systemFFmpegPath;
            }
            markExecutableAsInvalid(invalidFFmpegPaths, systemFFmpegPath, 'FFmpeg', 'dev system fallback verification failed');
        }
    }

    return null;
};
// FFprobe路径缓存，避免重复验证
let cachedFFprobePath: string | null = null;

/**
 * Execute FFmpeg command with the given parameters
 * @param params Array of FFmpeg command parameters
 * @param options Optional execution options
 * @returns Promise that resolves when the command completes
 */
export const execFFmpegCommand = async (
    params: string[],
    options?: {
        cwd?: string;
        outputEncoding?: string;
        onProgress?: (progress: any) => void;
        env?: Record<string, string>;
    }
): Promise<string> => {
    console.log('[execFFmpegCommand] ========== START ==========');
    console.log('[execFFmpegCommand] Called with params count:', params.length);

    const ffmpegPath = await getFFmpegPath();

    let cwd: string;
    if (options?.cwd && typeof options.cwd === 'string') {
        cwd = options.cwd;
    } else {
        await waitAppEnvReady();
        cwd = AppEnv.appRoot || process.cwd();
    }

    const outputEncoding = options?.outputEncoding || 'utf8';

    if (!Array.isArray(params)) {
        const error = new Error('params must be an array');
        Log.error('execFFmpegCommand: Invalid params type', { params, type: typeof params });
        throw error;
    }

    for (let i = 0; i < params.length; i++) {
        if (typeof params[i] !== 'string') {
            const error = new Error(`params[${i}] must be a string, got ${typeof params[i]}: ${params[i]}`);
            Log.error('execFFmpegCommand: Invalid param type', { index: i, value: params[i], type: typeof params[i] });
            throw error;
        }
    }

    const pathParamFlags = ['-i'];
    const normalizedParams = params.map((param, index) => {
        if (index > 0 && pathParamFlags.includes(params[index - 1])) {
            const normalized = normalizePath(param);
            if (normalized !== param) {
                Log.info('execFFmpegCommand: normalized input path', {
                    original: param.substring(0, 100),
                    normalized: normalized.substring(0, 100)
                });
            }
            return normalized;
        }

        if (index === params.length - 1) {
            const looksLikeOutputPath = param.includes('\\') || param.includes('/') || (
                param.includes('.') && param.length > 3 && !param.startsWith('-')
            );
            if (looksLikeOutputPath) {
                const outputDir = path.dirname(param);
                if (!fs.existsSync(outputDir)) {
                    fs.mkdirSync(outputDir, { recursive: true });
                }
                const normalized = normalizeOutputPath(param);
                if (normalized !== param) {
                    Log.info('execFFmpegCommand: normalized output path', {
                        original: param.substring(0, 100),
                        normalized: normalized.substring(0, 100)
                    });
                }
                return normalized;
            }
        }

        return param;
    });

    if (typeof cwd !== 'string') {
        const error = new Error(`cwd must be a string, got ${typeof cwd}`);
        Log.error('execFFmpegCommand: Invalid cwd type', { cwd, type: typeof cwd });
        throw error;
    }

    if (typeof ffmpegPath !== 'string') {
        const error = new Error(`ffmpegPath must be a string, got ${typeof ffmpegPath}`);
        Log.error('execFFmpegCommand: Invalid ffmpegPath type', { ffmpegPath, type: typeof ffmpegPath });
        throw error;
    }

    Log.info('execFFmpegCommand', { ffmpegPath, params: normalizedParams, cwd, outputEncoding });

    const env = buildRuntimeProcessEnv(options?.env);

    const runProcess = (executablePath: string) => {
        return new Promise<{ stdout: string; stderr: string; code: number | null }>((resolve, reject) => {
            try {
                const child = spawn(executablePath, normalizedParams, {
                    cwd,
                    shell: false,
                    env,
                });

                let stdout = '';
                let stderr = '';

                child.stdout?.on('data', (data) => {
                    const output = data.toString(outputEncoding as BufferEncoding);
                    stdout += output;

                    if (options?.onProgress) {
                        const timeMatch = output.match(/time=(\d{2}):(\d{2}):(\d{2}\.\d{2})/);
                        if (timeMatch) {
                            const [, hours, minutes, seconds] = timeMatch;
                            const totalSeconds = parseInt(hours) * 3600 + parseInt(minutes) * 60 + parseFloat(seconds);
                            options.onProgress({ currentTime: totalSeconds });
                        }
                    }
                });

                child.stderr?.on('data', (data) => {
                    const output = data.toString(outputEncoding as BufferEncoding);
                    stderr += output;
                    console.log('[FFmpeg stderr]', output.substring(0, 500));
                });

                child.on('close', (code) => {
                    Log.info('FFmpeg process completed', {
                        executablePath,
                        code,
                        stderrLength: stderr.length
                    });
                    console.log('[FFmpeg] exited with code:', code);
                    if (stderr.length > 0) {
                        console.log('[FFmpeg] stderr:\n', stderr.substring(0, 2000));
                    }
                    resolve({ stdout, stderr, code });
                });

                child.on('error', (error) => {
                    Log.error('FFmpeg process error', { executablePath, error });
                    reject(error);
                });
            } catch (error) {
                Log.error('execFFmpegCommand: spawn error', { executablePath, error });
                reject(error);
            }
        });
    };

    let activeFFmpegPath = ffmpegPath;
    let result = await runProcess(activeFFmpegPath);

    if (result.code !== 0 && result.code !== null && ILLEGAL_INSTRUCTION_EXIT_CODES.has(result.code)) {
        markExecutableAsInvalid(invalidFFmpegPaths, activeFFmpegPath, 'FFmpeg', `runtime exit code ${result.code}`);
        if (cachedFFmpegPath === activeFFmpegPath) {
            cachedFFmpegPath = null;
        }

        const fallbackPath = await findFallbackFFmpegPath([activeFFmpegPath]);
        if (fallbackPath) {
            Log.warn('execFFmpegCommand: retrying with fallback bundled FFmpeg', {
                failedPath: activeFFmpegPath,
                fallbackPath,
                code: result.code
            });
            cachedFFmpegPath = fallbackPath;
            activeFFmpegPath = fallbackPath;
            result = await runProcess(activeFFmpegPath);
        }
    }

    if (result.code === 0) {
        return result.stdout;
    }

    Log.error(`FFmpeg process exited with code ${result.code}`, result.stderr);
    throw new Error(`FFmpeg process exited with code ${result.code}: ${result.stderr}`);
};
/**
 * Extract audio from video file using FFmpeg
 * @param inputPath Path to the input video file
 * @param outputPath Path to save the extracted audio
 * @param options Audio extraction options
 * @returns Promise that resolves when audio extraction completes
 */
export const extractAudioFromVideo = async (
    inputPath: string,
    outputPath: string,
    options?: {
        audioCodec?: string;
        sampleRate?: number;
        channels?: number;
        bitrate?: string;
        onProgress?: (progress: any) => void;
    }
): Promise<string> => {
    const {
        audioCodec = 'mp3',
        sampleRate = 16000,
        channels = 1,
        bitrate = '128k',
        onProgress
    } = options || {};

    // 🔧 路径已经在 execFFmpegCommand 中自动转换，这里直接传递即可
    const params = [
        '-i', inputPath,
        '-vn', // No video
        '-acodec', audioCodec,
        '-ar', sampleRate.toString(),
        '-ac', channels.toString(),
        '-ab', bitrate,
        '-y', // Overwrite output
        outputPath
    ];

    return execFFmpegCommand(params, { onProgress });
};

/**
 * 验证FFmpeg是否可执行
 * @param ffmpegPath FFmpeg路径
 * @returns Promise<boolean> 是否可执行
 */
const verifyFFmpegExecutable = async (ffmpegPath: string): Promise<boolean> => {
    return verifyRuntimeExecutable(ffmpegPath, ['-version'], 'FFmpeg');
};
/**
 * Get the path to the FFmpeg executable
 * ⚠️ 优先使用程序自带的 FFmpeg，如果无法执行则回退到系统 FFmpeg
 * @returns Promise that resolves to the FFmpeg path
 */
export const getFFmpegPath = async (): Promise<string> => {
    if (cachedFFmpegPath && !invalidFFmpegPaths.has(cachedFFmpegPath)) {
        Log.debug('getFFmpegPath: using cached bundled path', { path: cachedFFmpegPath });
        return cachedFFmpegPath;
    }

    await waitAppEnvReady();
    cachedFFmpegPath = null;

    const bundledCandidates = getBundledExecutableCandidates(getFFmpegExecutablePath());
    Log.info('getFFmpegPath: checking bundled candidates', { bundledCandidates });

    for (const candidate of bundledCandidates) {
        if (!candidate || invalidFFmpegPaths.has(candidate) || !resourceExists(candidate)) {
            continue;
        }

        Log.info('getFFmpegPath: verifying bundled candidate', { candidate });
        if (await verifyBundledFFmpegCandidate(candidate)) {
            cachedFFmpegPath = candidate;
            Log.info('getFFmpegPath: using bundled candidate', { candidate });
            return candidate;
        }

        markExecutableAsInvalid(invalidFFmpegPaths, candidate, 'FFmpeg', 'startup verification failed');
    }

    if (isDevelopment()) {
        const systemFFmpegPath = isWin ? 'ffmpeg.exe' : 'ffmpeg';
        Log.warn('getFFmpegPath: bundled FFmpeg unavailable in dev, trying system FFmpeg', {
            systemFFmpegPath,
        });
        if (!invalidFFmpegPaths.has(systemFFmpegPath) && await verifyFFmpegExecutable(systemFFmpegPath)) {
            cachedFFmpegPath = systemFFmpegPath;
            Log.info('getFFmpegPath: using system FFmpeg in dev mode', { systemFFmpegPath });
            return systemFFmpegPath;
        }
        markExecutableAsInvalid(invalidFFmpegPaths, systemFFmpegPath, 'FFmpeg', 'dev system fallback verification failed');
    }

    const errorMsg = `无法找到可用的 FFmpeg: ${bundledCandidates.join(', ')}`;
    Log.error('getFFmpegPath', errorMsg);
    throw new Error(errorMsg);
};
/**
 * Get FFprobe path with fallback support
 * 1. Try bundled ffprobe
 * 2. Fallback to system ffprobe
 */
export const getFFprobePath = async (): Promise<string> => {
    if (cachedFFprobePath && !invalidFFprobePaths.has(cachedFFprobePath)) {
        Log.debug('getFFprobePath: using cached bundled path', { path: cachedFFprobePath });
        return cachedFFprobePath;
    }

    await waitAppEnvReady();
    cachedFFprobePath = null;

    const bundledCandidates = getBundledExecutableCandidates(getFFprobeExecutablePath());
    Log.info('getFFprobePath: checking bundled candidates', { bundledCandidates });

    for (const candidate of bundledCandidates) {
        if (!candidate || invalidFFprobePaths.has(candidate) || !resourceExists(candidate)) {
            continue;
        }

        Log.info('getFFprobePath: verifying bundled candidate', { candidate });
        if (await verifyBundledFFprobeCandidate(candidate)) {
            cachedFFprobePath = candidate;
            Log.info('getFFprobePath: using bundled candidate', { candidate });
            return candidate;
        }

        markExecutableAsInvalid(invalidFFprobePaths, candidate, 'FFprobe', 'startup verification failed');
    }

    if (isDevelopment()) {
        const systemFFprobePath = isWin ? 'ffprobe.exe' : 'ffprobe';
        Log.warn('getFFprobePath: bundled FFprobe unavailable in dev, trying system FFprobe', {
            systemFFprobePath,
        });
        if (!invalidFFprobePaths.has(systemFFprobePath) && await verifyRuntimeExecutable(systemFFprobePath, ['-version'], 'FFprobe')) {
            cachedFFprobePath = systemFFprobePath;
            Log.info('getFFprobePath: using system FFprobe in dev mode', { systemFFprobePath });
            return systemFFprobePath;
        }
        markExecutableAsInvalid(invalidFFprobePaths, systemFFprobePath, 'FFprobe', 'dev system fallback verification failed');
    }

    const errorMsg = `无法找到可用的 FFprobe: ${bundledCandidates.join(', ')}`;
    Log.error('getFFprobePath', errorMsg);
    throw new Error(errorMsg);
};
/**
 * Show item in folder using the system's file manager
 * @param filePath Path to the file to show
 * @returns boolean indicating success
 */
export const showItemInFolder = async (filePath: string): Promise<boolean> => {
    try {
        Log.info('showItemInFolder', { filePath });
        shell.showItemInFolder(filePath);
        return true;
    } catch (error) {
        Log.error('showItemInFolder error', error);
        return false;
    }
};

export default {
    execFFmpegCommand,
    extractAudioFromVideo,
    getFFmpegPath,
    getPythonPath,
    showItemInFolder,
};
