import { spawn, ChildProcess } from 'child_process';
import { platform } from 'os';
import path from 'path';
import { pathToFileURL } from 'url';
import fs from 'fs';
import AnsiToHtml from 'ansi-to-html';

// Load package.json for version
import packageJson from './package.json' with { type: 'json' };

export class MessageBoxHandle {
    readonly result: Promise<MessageBoxResult>;
    readonly pid: number | undefined;
    private child: ChildProcess;
    private _closed: boolean = false;

    constructor(child: ChildProcess, resultPromise: Promise<MessageBoxResult>) {
        this.child = child;
        this.pid = child.pid;
        this.result = resultPromise;
    }

    close(): boolean {
        if (this._closed) {
            return false;
        }
        this._closed = true;
        return this.child.kill();
    }

    get closed(): boolean {
        return this._closed;
    }
}

const activeDialogs = new Map<number, MessageBoxHandle>();

export function closeMessageBox(pid: number): boolean {
    const handle = activeDialogs.get(pid);
    if (handle) {
        return handle.close();
    }
    // Fallback to process.kill()
    try {
        process.kill(pid);
        return true;
    } catch {
        return false;
    }
}

export interface MessageBoxOptions {
    title?: string;           /** The title text displayed in the window title bar */
    message?: string;         /** The plain text message to display (optional if url or html is provided) */
    html?: string;            /** HTML content to display in the message box */
    url?: string;             /** URL to load external content (web URL or file path) */
    hash?: string;            /** Hash fragment to append to URL (requires url to be set). Leading # is optional. */
    // fullPage?: boolean;       /** Display HTML as a complete page without wrapping (default: false) */
    size?: {                  /** Window size in pixels */
        width: number;        /** Window width */
        height: number;       /** Window height */
    };
    pos?: {                   /** Window position */
        x: number;            /** Window X position in pixels */
        y: number;            /** Window Y position in pixels */
        screen?: number;      /** Optional screen index (0-based, Windows only) - NOT used by Rust binary */
    };
    zoom?: number;            /** Initial zoom level as percentage (100=100%, 150=150%, 50=50%, etc.) */
    autoSize?: boolean;       /** Automatically resize window to fit content (default: false) */
    alwaysOnTop?: boolean;    /** Keep window on top of other windows (default: false) */
    buttons?: string[];       /** Array of button labels to display (default: ['OK']) */
    defaultValue?: string;    /** Default value for input field when allowInput is true */
    inputPlaceholder?: string; /** Placeholder text (gray hint) shown in input field */
    allowInput?: boolean;     /** Enable an input field in the message box (default: false) */
    // devTools?: boolean;       /** Open DevTools for debugging (default: false) */
    timeout?: number;         /** Auto-close the message box after specified seconds */
    detach?: boolean;         /** Launch the message box detached from parent process */
    fullscreen?: boolean;     /** Start window in fullscreen mode (F11 to toggle, Escape to exit) */
    escapeCloses?: boolean;   /** Allow Escape key to close window (default: true). Always exits fullscreen first. */
    reset?: boolean;          /** Clear localStorage on startup (default: false) */
    icon?: string;            /** Path to window icon (PNG file). Default: icon.png in current directory */
    dev?: boolean;            /** Open DevTools automatically on startup (default: false) */
    debug?: boolean;          /** Return debug information (HTML, size) in result (default: false) */
    showVersion?: boolean;    /** Show version in window title (default: false) */
}

export interface MessageBoxResult {
    button: string;
    value?: string;
    form?: Record<string, any>;
    closed?: boolean;
    dismissed?: boolean;
    timeout?: boolean;
    debug?: {
        html: string;
        width: number;
        height: number;
        autoSize: boolean;
    };
}

/**
 * Internal function to create a MessageBoxHandle with child process
 * @param options Message box configuration
 * @returns MessageBoxHandle with result promise
 */
function createMessageBoxHandle(options: MessageBoxOptions): MessageBoxHandle {
    const isWindows = platform() === 'win32';
    const arch = process.arch;

    // Determine the binary name based on platform and architecture
    let binaryName: string;
    if (isWindows) {
        binaryName = 'msgernative.exe';
    } else if (arch === 'arm64') {
        binaryName = 'msgernative-linux-aarch64';
    } else {
        binaryName = 'msgernative';
    }

    const binaryPath = path.join(import.meta.dirname, 'msger-native', 'bin', binaryName);

    // Declare variables that will be used outside the Promise
    const childEnv = { ...process.env } as Record<string, any>;
    if ('NODE_OPTIONS' in childEnv) {
        delete childEnv.NODE_OPTIONS;
    }

    let child: ChildProcess;
    let stdout = '';
    let stderr = '';

    const resultPromise = new Promise<MessageBoxResult>((resolve, reject) => {
        // Check if binary exists
        if (!fs.existsSync(binaryPath)) {
            reject(new Error(`Binary not found: ${binaryPath}`));
            return;
        }

        // On Unix systems, check if binary is executable
        if (!isWindows) {
            try {
                fs.accessSync(binaryPath, fs.constants.X_OK);
            } catch (error) {
                reject(new Error(
                    `Binary is not executable: ${binaryPath}\n` +
                    `Please run: sudo chmod +x ${binaryPath}`
                ));
                return;
            }
        }

        // Spawn the Rust binary
        // Spawn child with a sanitized environment. Electron will print
        // warnings if NODE_OPTIONS is present in the environment of a
        // packaged app. Remove NODE_OPTIONS to avoid that warning.
        // When detached, use 'ignore' for stdout/stderr so parent can exit
        // We only need stdin to send the options, not to receive results
        child = spawn(binaryPath, [], {
            stdio: options.detach ? ['pipe', 'ignore', 'ignore'] : ['pipe', 'pipe', 'pipe'],
            detached: options.detach || false,
            env: childEnv
        });

        // If detached, unref the child so parent can exit
        if (options.detach) {
            child.unref();
        }

        // Collect stdout (only if not detached)
        child.stdout?.on('data', (data) => {
            stdout += data.toString();
        });

        // Collect stderr (only if not detached)
        child.stderr?.on('data', (data) => {
            stderr += data.toString();
        });

        // Handle process completion
        child.on('close', (code) => {
            // code is null when process is killed via handle.close()
            if (code === null) {
                resolve({ button: 'closed', closed: true });
                return;
            }

            if (code !== 0) {
                reject(new Error(`msger exited with code ${code}: ${stderr}`));
                return;
            }

            try {
                const result = JSON.parse(stdout.trim()) as MessageBoxResult;
                resolve(result);
            } catch (error: any) {
                reject(new Error(`Failed to parse result: ${error.message}\nOutput: ${stdout}`));
            }
        });

        // Handle process errors
        child.on('error', (error) => {
            reject(new Error(`Failed to spawn msger: ${error.message}`));
        });

        // Handle stdin errors (e.g., EPIPE when child dies early)
        child.stdin.on('error', (error: any) => {
            // Don't reject here - the 'close' or 'error' event will handle it
            // This prevents unhandled EPIPE errors
        });

        // Send options to stdin
        try {
            // If autoSize is not explicitly set, enable it when no size is specified
            const optionsToSend = { ...options };
            if (optionsToSend.autoSize === undefined && !optionsToSend.size) {
                optionsToSend.autoSize = true;
            }
            
            // Convert ANSI escape sequences to HTML if message is provided
            // ansi-to-html will escape HTML characters and convert ANSI codes to colored spans
            // Only do this if the message contains actual ANSI codes to preserve plain text as-is
            if (optionsToSend.message && !optionsToSend.html) {
                // Check if message actually contains ANSI codes
                if (optionsToSend.message.includes('\x1b[')) {
                    const convert = new AnsiToHtml({ fg: '#000000', bg: '#ffffff' });
                    let htmlContent = convert.toHtml(optionsToSend.message);
                    // ansi-to-html preserves newlines, so we need to convert them to <br> for HTML
                    htmlContent = htmlContent.replace(/\r?\n/g, '<br>');
                    // Move the converted content to html field and clear message
                    optionsToSend.html = htmlContent;
                    delete optionsToSend.message;
                }
            }
            
            // Convert URL to absolute file:// URL if it's a file path
            if (optionsToSend.url && !optionsToSend.url.startsWith('http://') && !optionsToSend.url.startsWith('https://') && !optionsToSend.url.startsWith('file://')) {
                // It's a file path, convert to absolute file:// URL
                // Resolve to absolute path (handles both / and \ separators)
                const absolutePath = path.resolve(optionsToSend.url);

                // Check if file exists
                if (!fs.existsSync(absolutePath)) {
                    throw new Error(`File not found: ${absolutePath}`);
                }

                // Convert to file:// URL and normalize for Windows
                let fileUrl = pathToFileURL(absolutePath).href;

                // Ensure proper URL encoding
                optionsToSend.url = fileUrl;

                // Debug logging (uncomment if needed)
                // console.error(`[DEBUG] Path conversion: ${optionsToSend.url} → ${absolutePath} → ${fileUrl}`);
            }

            // Append hash to URL if specified
            if (optionsToSend.hash && optionsToSend.url) {
                // Strip any existing hash from the base URL
                optionsToSend.url = optionsToSend.url.split('#')[0];
                // Remove leading # from hash if present to avoid ##
                const hashValue = optionsToSend.hash.startsWith('#') ? optionsToSend.hash.substring(1) : optionsToSend.hash;
                optionsToSend.url = `${optionsToSend.url}#${hashValue}`;
                // Remove hash from options as it's now part of URL
                delete optionsToSend.hash;
            } else if (optionsToSend.hash && !optionsToSend.url) {
                throw new Error('hash option requires url to be set');
            }

            // Flatten size object for Rust binary (expects width/height as top-level fields)
            const rustOptions: any = { ...optionsToSend };
            
            // Add version to title only if showVersion flag is set
            if (rustOptions.showVersion) {
                if (rustOptions.title) {
                    rustOptions.title = `${rustOptions.title} [msger v${packageJson.version}]`;
                } else {
                    rustOptions.title = `Message [msger v${packageJson.version}]`;
                }
                delete rustOptions.showVersion; // Don't send this to Rust binary
            }
            
            if (rustOptions.size) {
                rustOptions.width = rustOptions.size.width;
                rustOptions.height = rustOptions.size.height;
                delete rustOptions.size;
            }
            if (rustOptions.pos) {
                // Rust expects pos fields directly
                rustOptions.pos = {
                    x: rustOptions.pos.x,
                    y: rustOptions.pos.y,
                    screen: rustOptions.pos.screen
                };
            }
            
            const jsonToSend = JSON.stringify(rustOptions);
            // DEBUG: Uncomment to see what's being sent
            // console.error('[DEBUG] JSON sent to native binary:', jsonToSend);
            
            // If detached, resolve immediately after stdin is closed
            if (options.detach) {
                child.stdin.write(jsonToSend);
                child.stdin.end(() => {
                    resolve({ button: 'detached', dismissed: false });
                });
            } else {
                child.stdin.write(jsonToSend);
                child.stdin.end();
            }
        } catch (error: any) {
            reject(new Error(`Failed to write to stdin: ${error.message}`));
        }
    });

    return new MessageBoxHandle(child, resultPromise);
}

/**
 * Show a message box dialog using native Rust implementation (extended version)
 * @param options Message box configuration
 * @returns MessageBoxHandle with result promise and control methods
 */
export function showMessageBoxEx(options: MessageBoxOptions): MessageBoxHandle {
    const handle = createMessageBoxHandle(options);

    // Register in activeDialogs with auto-cleanup
    if (handle.pid !== undefined) {
        activeDialogs.set(handle.pid, handle);

        // Auto-cleanup when result resolves
        handle.result.finally(() => {
            if (handle.pid !== undefined) {
                activeDialogs.delete(handle.pid);
            }
        });
    }

    return handle;
}

/**
 * Show a message box dialog using native Rust implementation
 * @param options Message box configuration
 * @returns Promise that resolves with user's response
 */
export async function showMessageBox(options: MessageBoxOptions): Promise<MessageBoxResult> {
    return showMessageBoxEx(options).result;
}
