from __future__ import annotations

import argparse
import logging
import platform
import shutil
import subprocess
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass, field
from functools import cached_property
from pathlib import Path
from typing import Final

from sfi.pyprojectparse.pyprojectparse import Project, Solution

is_windows = platform.system() == "Windows"
is_linux = platform.system() == "Linux"
is_macos = platform.system() == "Darwin"
ext = ".exe" if is_windows else ""

logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger(__name__)
cwd = Path.cwd()

# Default cache directory
_DEFAULT_BUILD_DIR = Path(".cbuild")
_DEFAULT_OUTPUT_DIR = Path("dist")

# Platform templates
_WINDOWS_GUI_TEMPLATE: str = r"""#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <locale.h>

#define MAX_PATH_LEN 4096
#define MAX_ERROR_LEN 8192

// Check if Python runtime exists
static int check_python_runtime(const char* runtime_path) {
    return GetFileAttributesA(runtime_path) != INVALID_FILE_ATTRIBUTES;
}

// Build Python command line
static void build_python_command(
    char* cmd,
    const char* exe_dir,
    const char* entry_file,
    int is_debug,
    LPSTR lpCmdLine
) {
    char python_runtime[MAX_PATH_LEN];
    char script_path[MAX_PATH_LEN];

    // Build Python interpreter path
    // Debug mode: use python.exe to show console output
    // Production GUI mode: use pythonw.exe to hide console window
    if (is_debug) {
        snprintf(python_runtime, MAX_PATH_LEN, "%s\\runtime\\python.exe", exe_dir);
    } else {
        snprintf(python_runtime, MAX_PATH_LEN, "%s\\runtime\\pythonw.exe", exe_dir);
    }

    // Build startup script path
    snprintf(script_path, MAX_PATH_LEN, "%s\\%s", exe_dir, entry_file);

    // Build command line
    if (is_debug) {
        // Debug mode: don't redirect output, let output show in console
        snprintf(cmd, MAX_PATH_LEN * 2, "\"%s\" -u \"%s\"%s%s",
            python_runtime, script_path,
            lpCmdLine && strlen(lpCmdLine) > 0 ? " " : "",
            lpCmdLine && strlen(lpCmdLine) > 0 ? lpCmdLine : "");
    } else {
        // Production mode: redirect all output to pipe
        snprintf(cmd, MAX_PATH_LEN * 2, "\"%s\" -u \"%s\"%s%s 2>&1",
            python_runtime, script_path,
            lpCmdLine && strlen(lpCmdLine) > 0 ? " " : "",
            lpCmdLine && strlen(lpCmdLine) > 0 ? lpCmdLine : "");
    }
}

// Read process output
static void read_process_output(HANDLE hPipe, char* output, int max_len) {
    DWORD bytes_read;
    output[0] = '\0';

    while (
    ReadFile(hPipe, output + strlen(output), max_len - strlen(output) - 1, &bytes_read, NULL) && bytes_read > 0) {
        output[bytes_read] = '\0';
    }
}

// Show message box (support UTF-8 encoding)
static void show_message_box(const char* title, const char* message) {
    // Convert UTF-8 to UTF-16 using MultiByteToWideChar
    int title_len = MultiByteToWideChar(CP_UTF8, 0, title, -1, NULL, 0);
    int msg_len = MultiByteToWideChar(CP_UTF8, 0, message, -1, NULL, 0);

    if (title_len > 0 && msg_len > 0) {
        wchar_t* wtitle = (wchar_t*)malloc(title_len * sizeof(wchar_t));
        wchar_t* wmsg = (wchar_t*)malloc(msg_len * sizeof(wchar_t));

        if (wtitle && wmsg) {
            MultiByteToWideChar(CP_UTF8, 0, title, -1, wtitle, title_len);
            MultiByteToWideChar(CP_UTF8, 0, message, -1, wmsg, msg_len);
            MessageBoxW(NULL, wmsg, wtitle, MB_ICONERROR | MB_OK);
        }

        if (wtitle) free(wtitle);
        if (wmsg) free(wmsg);
    } else {
        // Conversion failed, use ANSI version
        MessageBoxA(NULL, message, title, MB_ICONERROR | MB_OK);
    }
}

// Windows GUI entry point
int APIENTRY WinMain(
    HINSTANCE hInstance,
    HINSTANCE hPrevInstance,
    LPSTR lpCmdLine,
    int nCmdShow
) {
    char exe_dir[MAX_PATH_LEN];
    char cmd[MAX_PATH_LEN * 2];
    char error_output[MAX_ERROR_LEN] = "";
    char error_msg[MAX_ERROR_LEN];
    STARTUPINFOA si = {0};
    PROCESS_INFORMATION pi = {0};
    SECURITY_ATTRIBUTES sa = {0};
    HANDLE hReadPipe = NULL, hWritePipe = NULL;
    BOOL success;

    // Get executable directory
    GetModuleFileNameA(NULL, exe_dir, MAX_PATH_LEN);
    char* last_slash = strrchr(exe_dir, '\\');
    if (last_slash) {
        *last_slash = '\0';
    }

    // Check Python runtime
    if (!check_python_runtime(exe_dir)) {
        show_message_box(
            "Application Error",
            "Python runtime not found.\n\n"
            "Please ensure the application is installed correctly and the runtime directory exists."
        );
        return 1;
    }

    // Build Python command
    build_python_command(cmd, exe_dir, "${ENTRY_FILE}", ${DEBUG_MODE}, lpCmdLine);

    // Setup startup info
    si.cb = sizeof(si);

    // Create pipe for capturing output (only in production mode)
    if (!${DEBUG_MODE}) {
        sa.nLength = sizeof(SECURITY_ATTRIBUTES);
        sa.bInheritHandle = TRUE;
        sa.lpSecurityDescriptor = NULL;

        if (!CreatePipe(&hReadPipe, &hWritePipe, &sa, 0)) {
            show_message_box("Error", "Failed to create pipe for error output.");
            return 1;
        }

        si.dwFlags = STARTF_USESTDHANDLES;
        si.hStdError = hWritePipe;
        si.hStdOutput = hWritePipe;
    }

    // Create Python process
    // Debug mode: do not use CREATE_NO_WINDOW, let python.exe create console
    // Production GUI mode: use CREATE_NO_WINDOW to ensure no console is created
    DWORD createFlags = ${DEBUG_MODE} ? 0 : CREATE_NO_WINDOW;
    success = CreateProcessA(
        NULL, cmd, NULL, NULL, ${DEBUG_MODE} ? TRUE : FALSE,
        createFlags,
        NULL, exe_dir, &si, &pi
    );

    if (!success) {
        DWORD error = GetLastError();
        snprintf(error_msg, MAX_ERROR_LEN,
            "Failed to start Python process.\n\n"
            "Error code: %lu\n"
            "Command: %s",
            error, cmd);
        show_message_box("Application Error", error_msg);
        if (!${DEBUG_MODE}) {
            CloseHandle(hWritePipe);
            CloseHandle(hReadPipe);
        }
        return 1;
    }

    // Read error output (only in non-debug mode)
    if (!${DEBUG_MODE}) {
        CloseHandle(hWritePipe);
        read_process_output(hReadPipe, error_output, MAX_ERROR_LEN);
        CloseHandle(hReadPipe);
    }

    // Wait for process to end
    WaitForSingleObject(pi.hProcess, INFINITE);

    // Get exit code
    DWORD exit_code;
    GetExitCodeProcess(pi.hProcess, &exit_code);

#if ${DEBUG_MODE}
    fprintf(stderr, "DEBUG: Python process exited with code: %lu\n", exit_code);
#endif

    // Cleanup
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);

    // If process exits abnormally and there is error output, display error message
    if (exit_code != 0 && strlen(error_output) > 0) {
        // Truncate the last error message (up to 2000 characters)
        size_t error_len = strlen(error_output);
        if (error_len > 2000) {
            error_output[2000] = '\0';
        }
        show_message_box("Application Error", error_output);
        return exit_code;
    }

    return exit_code;
}
"""

_WINDOWS_CONSOLE_TEMPLATE: str = r"""#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
#include <locale.h>

#define MAX_PATH_LEN 4096
#define MAX_ERROR_LEN 8192

// Set console encoding to UTF-8
static void setup_encoding() {
    SetConsoleOutputCP(CP_UTF8);
    SetConsoleCP(CP_UTF8);
    setlocale(LC_ALL, ".UTF8");
}

// Check if Python runtime exists
static int check_python_runtime(const char* runtime_path) {
    return GetFileAttributesA(runtime_path) != INVALID_FILE_ATTRIBUTES;
}

// Build Python command line
static void build_python_command(
    char* cmd,
    const char* exe_dir,
    const char* entry_file,
    int argc,
    char* argv[]
) {
    char python_runtime[MAX_PATH_LEN];
    char script_path[MAX_PATH_LEN];
    char* p = cmd;
    size_t remaining = MAX_PATH_LEN * 2;
    int len;

    // Build Python interpreter path
    snprintf(python_runtime, MAX_PATH_LEN, "%s\\runtime\\python.exe", exe_dir);

    // Build startup script path
    snprintf(script_path, MAX_PATH_LEN, "%s\\%s", exe_dir, entry_file);

    // Base command
    len = snprintf(p, remaining, "\"%s\" -u \"%s\"", python_runtime, script_path);
    p += len;
    remaining -= len;

    // Append arguments
    for (int i = 1; i < argc && remaining > 0; i++) {
        len = snprintf(p, remaining, " \"%s\"", argv[i]);
        if (len < 0 || (size_t)len >= remaining) break;
        p += len;
        remaining -= len;
    }
}

// Read process output
static void read_process_output(HANDLE hPipe, char* output, int max_len) {
    DWORD bytes_read;
    output[0] = '\0';

    while (ReadFile(hPipe, output + strlen(output), max_len - strlen(output) - 1, &bytes_read, NULL) && bytes_read > 0) {
        output[bytes_read] = '\0';
    }
}

// Windows Console entry point
int main(int argc, char* argv[]) {
    char exe_dir[MAX_PATH_LEN];
    char cmd[MAX_PATH_LEN * 2];
    STARTUPINFOA si = {0};
    PROCESS_INFORMATION pi = {0};
    BOOL success;
    char* last_slash;

    // Setup
    setup_encoding();
    si.cb = sizeof(si);

    // Get executable directory
    GetModuleFileNameA(NULL, exe_dir, MAX_PATH_LEN);
    if ((last_slash = strrchr(exe_dir, '\\'))) *last_slash = '\0';

    // Check Python runtime
    if (!check_python_runtime(exe_dir)) {
        fprintf(stderr, "Error: Python runtime not found at %s\\runtime\\\n", exe_dir);
        fprintf(stderr, "Please ensure the application is installed correctly.\n");
        return 1;
    }

    // Build and execute Python command
    build_python_command(cmd, exe_dir, "${ENTRY_FILE}", argc, argv);

#if ${DEBUG_MODE}
    // Debug output
#ifdef _WIN32
    {
        int wcmd_len = MultiByteToWideChar(CP_UTF8, 0, cmd, -1, NULL, 0);
        if (wcmd_len > 0) {
            wchar_t* wcmd = (wchar_t*)malloc(wcmd_len * sizeof(wchar_t));
            MultiByteToWideChar(CP_UTF8, 0, cmd, -1, wcmd, wcmd_len);
            fwprintf(stderr, L"DEBUG: Command to execute: %ls\n", wcmd);
            free(wcmd);
        }
    }
#else
    fprintf(stderr, "DEBUG: Command to execute: %s\n", cmd);
#endif
#endif

    // Create Python process
    // TRUE - inherit handles so Python can write to console
    success = CreateProcessA(
        NULL, cmd, NULL, NULL, TRUE,
        0, NULL, exe_dir, &si, &pi
    );

    if (!success) {
        DWORD error = GetLastError();
        fprintf(stderr, "Error: Failed to start Python process.\n");
        fprintf(stderr, "Error code: %lu\n", error);
        fprintf(stderr, "Command: %s\n", cmd);
        return 1;
    }

    // In non-debug mode for console apps, we don't use pipes, so no error output capture
    // All output (stdout/stderr) goes directly to console

    // Wait for process to end
    WaitForSingleObject(pi.hProcess, INFINITE);

    // Get exit code
    DWORD exit_code;
    GetExitCodeProcess(pi.hProcess, &exit_code);

#if ${DEBUG_MODE}
    fprintf(stderr, "DEBUG: Python process exited with code: %lu\n", exit_code);
#endif

    // Cleanup
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);

    // In non-debug mode, we don't capture output, so just report exit code if non-zero
    if (exit_code != 0) {
        fprintf(stderr, "\nApplication exited with error code: %lu\n", exit_code);
        fprintf(stderr, "Check console output above for details.\n");
        return exit_code;
    }

    return exit_code;
}
"""

_UNIX_GUI_TEMPLATE: str = r"""#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>

#define MAX_PATH_LEN 4096
#define MAX_ERROR_LEN 8192

// Build Python command line
static void build_python_command(
    char* cmd,
    const char* exe_dir,
    const char* entry_file,
    int argc,
    char* argv[]
) {
    char script_path[MAX_PATH_LEN];
    char* p = cmd;
    size_t remaining = MAX_PATH_LEN * 3;
    int len;

    // Build startup script path
    snprintf(script_path, MAX_PATH_LEN, "%s/%s", exe_dir, entry_file);

    // Build log file path (record error information)
    char log_file[MAX_PATH_LEN];
    snprintf(log_file, MAX_PATH_LEN, "%s/.error_log", exe_dir);

    // Build command line - use system python3 instead of bundled runtime
    // GUI mode uses nohup and background execution, error output to log
    len = snprintf(p, remaining,
        "cd \"%s\" && nohup python3 -u \"%s\"",
        exe_dir, script_path);
    p += len;
    remaining -= len;

    // Append command line arguments
    for (int i = 1; i < argc && remaining > 0; i++) {
        len = snprintf(p, remaining, " \"%s\"", argv[i]);
        if (len < 0 || (size_t)len >= remaining) break;
        p += len;
        remaining -= len;
    }

    // Append log redirection
    if (remaining > 0) {
        snprintf(p, remaining, " >\"%s\" 2>&1 &", log_file);
    }
}

// Unix GUI entry point
int main(int argc, char* argv[]) {
    char exe_dir[MAX_PATH_LEN];
    char cmd[MAX_PATH_LEN * 3];
    char log_file[MAX_PATH_LEN];
    pid_t pid;
    int status;
    char* last_slash;

    // Get executable directory
    if (realpath("/proc/self/exe", exe_dir) == NULL &&
        realpath(argv[0], exe_dir) == NULL) {
        fprintf(stderr, "Error: Cannot determine executable directory\n");
        return 1;
    }

    // Remove executable name, keep only directory
    if ((last_slash = strrchr(exe_dir, '/'))) *last_slash = '\0';

    // Build and execute Python command
    build_python_command(cmd, exe_dir, "${ENTRY_FILE}", argc, argv);
    snprintf(log_file, MAX_PATH_LEN, "%s/.error_log", exe_dir);

    // Execute Python process in background
    pid = fork();
    if (pid == 0) {
        // Child process: execute Python command and exit immediately
        int ret = system(cmd);
        _exit(WEXITSTATUS(ret));
    } else if (pid < 0) {
        fprintf(stderr, "Error: Failed to fork process\n");
        return 1;
    }

    // Wait for a short time to check if process failed to start
    sleep(1);
    if (waitpid(pid, &status, WNOHANG) == pid) {
        // Process has already exited, read error log
        FILE* fp = fopen(log_file, "r");
        if (fp) {
            char error_msg[MAX_ERROR_LEN];
            if (fgets(error_msg, sizeof(error_msg), fp)) {
                fprintf(stderr, "Application failed to start:\n%s\n", error_msg);
            }
            fclose(fp);
        } else {
            fprintf(stderr, "Application failed to start (exit code: %d)\n", WEXITSTATUS(status));
        }
        return WEXITSTATUS(status);
    }

    // Parent process exits immediately
    return 0;
}
"""

_UNIX_CONSOLE_TEMPLATE: str = r"""#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>

#define MAX_PATH_LEN 4096

// Build Python command line
static void build_python_command(
    char* cmd,
    const char* exe_dir,
    const char* entry_file,
    int argc,
    char* argv[]
) {
    char script_path[MAX_PATH_LEN];
    char* p = cmd;
    size_t remaining = MAX_PATH_LEN * 2;
    int len;

    // Build startup script path
    snprintf(script_path, MAX_PATH_LEN, "%s/%s", exe_dir, entry_file);

    // Build command line - use system python3 instead of bundled runtime
    // add -u parameter for real-time output capture
    len = snprintf(p, remaining,
        "cd \"%s\" && python3 -u \"%s\"",
        exe_dir, script_path);
    p += len;
    remaining -= len;

    // Append command line arguments
    for (int i = 1; i < argc && remaining > 0; i++) {
        len = snprintf(p, remaining, " \"%s\"", argv[i]);
        if (len < 0 || (size_t)len >= remaining) break;
        p += len;
        remaining -= len;
    }
}

// Unix Console entry point
int main(int argc, char* argv[]) {
    char exe_dir[MAX_PATH_LEN];
    char cmd[MAX_PATH_LEN * 3];
    pid_t pid;
    int status;
    char* last_slash;

    // Get executable directory
    if (realpath("/proc/self/exe", exe_dir) == NULL) {
        fprintf(stderr, "Error: Cannot determine executable directory\n");
        return 1;
    }

    // Remove executable name, keep only directory
    if ((last_slash = strrchr(exe_dir, '/'))) *last_slash = '\0';

    // Build and execute Python command
    build_python_command(cmd, exe_dir, "${ENTRY_FILE}", argc, argv);

    // Fork and execute Python process
    pid = fork();
    if (pid == 0) {
        // Child process: execute Python command
        execl("/bin/sh", "sh", "-c", cmd, NULL);
        fprintf(stderr, "Error: Failed to execute Python process\n");
        _exit(127); // If execl fails
    } else if (pid < 0) {
        fprintf(stderr, "Error: Failed to fork process\n");
        return 1;
    }

    // Parent process: wait for child process to end
    waitpid(pid, &status, 0);

    int exit_code = WEXITSTATUS(status);
    if (exit_code != 0) {
        fprintf(stderr, "\nApplication exited abnormally, error code: %d\n", exit_code);
        fprintf(stderr, "Please check the error information above for details.\n");
    }

    return exit_code;
}
"""

_MACOS_GUI_TEMPLATE: str = r"""#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <CoreFoundation/CoreFoundation.h>
#include <ApplicationServices/ApplicationServices.h>

#define MAX_PATH_LEN 4096
#define MAX_ERROR_LEN 8192

// Check if Python interpreter exists
static int check_python_runtime(const char* runtime_path) {
    struct stat st;
    return stat(runtime_path, &st) == 0 && (st.st_mode & S_IXUSR);
}

// Get executable directory (macOS specific)
static void get_exe_dir_macos(char* exe_dir, size_t size) {
    CFBundleRef bundle = CFBundleGetMainBundle();
    if (bundle) {
        CFURLRef url = CFBundleCopyBundleURL(bundle);
        if (url) {
            CFStringRef path = CFURLCopyFileSystemPath(url, kCFURLPOSIXPathStyle);
            if (path) {
                CFStringGetCString(path, exe_dir, size, kCFStringEncodingUTF8);
                CFRelease(path);
            }
            CFRelease(url);
        }
    } else {
        // If not in bundle, get current working directory
        if (getcwd(exe_dir, size) == NULL) {
            exe_dir[0] = '\0';
        }
    }

    // Remove Contents/MacOS suffix of bundle
    char* macos_path = strstr(exe_dir, ".app/Contents/MacOS");
    if (macos_path) {
        *macos_path = '\0';
    }
}

// Display macOS error dialog
static void show_error_dialog(const char* message) {
    CFStringRef cf_message = CFStringCreateWithCString(
        NULL, message, kCFStringEncodingUTF8
    );
    CFOptionFlags response;
    CFUserNotificationDisplayAlert(
        0,
        kCFUserNotificationStopAlertLevel,
        NULL, NULL, NULL,
        CFSTR("Application Error"),
        cf_message,
        CFSTR("OK"), NULL, NULL, &response
    );
    CFRelease(cf_message);
}

// Build Python command line
static void build_python_command(
    char* cmd,
    const char* exe_dir,
    const char* entry_file,
    int argc,
    char* argv[]
) {
    char python_runtime[MAX_PATH_LEN];
    char script_path[MAX_PATH_LEN];
    char log_file[MAX_PATH_LEN];
    char* p = cmd;
    size_t remaining = MAX_PATH_LEN * 2;
    int len;

    // macOS Python path
    snprintf(python_runtime, MAX_PATH_LEN, "%s/runtime/bin/python3", exe_dir);
    snprintf(script_path, MAX_PATH_LEN, "%s/%s", exe_dir, entry_file);

    // Build log file path
    snprintf(log_file, MAX_PATH_LEN, "%s/.error_log", exe_dir);

    // Build command line (GUI mode uses nohup and background execution, error output to log)
    len = snprintf(p, remaining,
        "cd \"%s\" && nohup \"%s\" -u \"%s\"",
        exe_dir, python_runtime, script_path);
    p += len;
    remaining -= len;

    // Append command line arguments
    for (int i = 1; i < argc && remaining > 0; i++) {
        len = snprintf(p, remaining, " \"%s\"", argv[i]);
        if (len < 0 || (size_t)len >= remaining) break;
        p += len;
        remaining -= len;
    }

    // Append log redirection
    if (remaining > 0) {
        snprintf(p, remaining, " >\"%s\" 2>&1 &", log_file);
    }
}

// macOS GUI entry point
int main(int argc, char* argv[]) {
    char exe_dir[MAX_PATH_LEN];
    char cmd[MAX_PATH_LEN * 3];
    char log_file[MAX_PATH_LEN];
    char python_runtime[MAX_PATH_LEN];
    pid_t pid;
    int status;

    // Get executable directory
    get_exe_dir_macos(exe_dir, MAX_PATH_LEN);

    // Check Python runtime
    snprintf(python_runtime, MAX_PATH_LEN, "%s/runtime/bin/python3", exe_dir);
    if (!check_python_runtime(python_runtime)) {
        char error_msg[MAX_PATH_LEN];
        snprintf(error_msg, MAX_PATH_LEN,
            "Python runtime not found.\n\n"
            "Please ensure the application is installed correctly and the runtime directory is at:\n%s/runtime/bin/",
            exe_dir);
        show_error_dialog(error_msg);
        return 1;
    }

    // Build and execute Python command
    build_python_command(cmd, exe_dir, "${ENTRY_FILE}", argc, argv);
    snprintf(log_file, MAX_PATH_LEN, "%s/.error_log", exe_dir);

    // Execute Python process in background
    pid = fork();
    if (pid == 0) {
        // Child process
        int ret = system(cmd);
        _exit(WEXITSTATUS(ret));
    } else if (pid < 0) {
        char error_msg[MAX_PATH_LEN];
        snprintf(error_msg, MAX_PATH_LEN, "Failed to create process.\nError: %s", strerror(errno));
        show_error_dialog(error_msg);
        return 1;
    }

    // Wait for a short time to check if process failed to start
    sleep(1);
    if (waitpid(pid, &status, WNOHANG) == pid) {
        // Process has already exited, read error log
        FILE* fp = fopen(log_file, "r");
        if (fp) {
            char error_msg[MAX_ERROR_LEN];
            if (fgets(error_msg, sizeof(error_msg), fp)) {
                // Remove newline
                char* newline = strchr(error_msg, '\n');
                if (newline) *newline = '\0';
                char full_error[MAX_PATH_LEN + MAX_ERROR_LEN];
                snprintf(full_error, sizeof(full_error),
                    "Application failed to start.\n\nError:\n%s", error_msg);
                show_error_dialog(full_error);
            }
            fclose(fp);
        } else {
            char error_msg[MAX_PATH_LEN];
            snprintf(error_msg, MAX_PATH_LEN,
                "Application failed to start.\n\nExit code: %d",
                WEXITSTATUS(status));
            show_error_dialog(error_msg);
        }
        return WEXITSTATUS(status);
    }

    return 0;
}
"""

_MACOS_CONSOLE_TEMPLATE: str = r"""#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <errno.h>
#include <CoreFoundation/CoreFoundation.h>

#define MAX_PATH_LEN 4096

// Check if Python interpreter exists
static int check_python_runtime(const char* runtime_path) {
    struct stat st;
    return stat(runtime_path, &st) == 0 && (st.st_mode & S_IXUSR);
}

// Get executable directory (macOS specific)
static void get_exe_dir_macos(char* exe_dir, size_t size) {
    CFBundleRef bundle = CFBundleGetMainBundle();
    if (bundle) {
        CFURLRef url = CFBundleCopyBundleURL(bundle);
        if (url) {
            CFStringRef path = CFURLCopyFileSystemPath(url, kCFURLPOSIXPathStyle);
            if (path) {
                CFStringGetCString(path, exe_dir, size, kCFStringEncodingUTF8);
                CFRelease(path);
            }
            CFRelease(url);
        }
    } else {
        // Get current working directory if not in bundle
        if (getcwd(exe_dir, size) == NULL) {
            exe_dir[0] = '\0';
        }
    }
}

// Build Python command line
static void build_python_command(
    char* cmd,
    const char* exe_dir,
    const char* entry_file,
    int argc,
    char* argv[]
) {
    char python_runtime[MAX_PATH_LEN];
    char script_path[MAX_PATH_LEN];
    char* p = cmd;
    size_t remaining = MAX_PATH_LEN;
    int len;

    snprintf(python_runtime, MAX_PATH_LEN, "%s/runtime/bin/python3", exe_dir);
    snprintf(script_path, MAX_PATH_LEN, "%s/%s", exe_dir, entry_file);

    // Build command line (add -u parameter for real-time output capture)
    len = snprintf(p, remaining,
        "cd \"%s\" && \"%s\" -u \"%s\"",
        exe_dir, python_runtime, script_path);
    p += len;
    remaining -= len;

    // Append command line arguments
    for (int i = 1; i < argc && remaining > 0; i++) {
        len = snprintf(p, remaining, " \"%s\"", argv[i]);
        if (len < 0 || (size_t)len >= remaining) break;
        p += len;
        remaining -= len;
    }
}

// macOS Console entry point
int main(int argc, char* argv[]) {
    char exe_dir[MAX_PATH_LEN];
    char cmd[MAX_PATH_LEN * 3];
    char python_runtime[MAX_PATH_LEN];
    pid_t pid;
    int status;

    // Get executable directory
    get_exe_dir_macos(exe_dir, MAX_PATH_LEN);

    // Check Python runtime
    snprintf(python_runtime, MAX_PATH_LEN, "%s/runtime/bin/python3", exe_dir);
    if (!check_python_runtime(python_runtime)) {
        fprintf(stderr, "Error: Python runtime not found at %s/runtime/bin/\n", exe_dir);
        fprintf(stderr, "Please ensure the application is installed correctly.\n");
        return 1;
    }

    // Build and execute Python command
    build_python_command(cmd, exe_dir, "${ENTRY_FILE}", argc, argv);

    // Fork and execute Python process
    pid = fork();
    if (pid == 0) {
        execl("/bin/sh", "sh", "-c", cmd, NULL);
        fprintf(stderr, "Error: Failed to execute Python process\n");
        _exit(127);
    } else if (pid < 0) {
        fprintf(stderr, "Error: Failed to create process: %s\n", strerror(errno));
        return 1;
    }

    waitpid(pid, &status, 0);

    int exit_code = WEXITSTATUS(status);
    if (exit_code != 0) {
        fprintf(stderr, "\nApplication exited abnormally, error code: %d\n", exit_code);
        fprintf(stderr, "Please check the error information above for details.\n");
    }

    return exit_code;
}
"""

ENTRY_FILE_TEMPLATE = r"""
import os
import sys
from pathlib import Path

# Setup environment
cwd = Path.cwd()
site_dirs = [cwd / "site-packages", cwd / "lib"]
dirs = [cwd, cwd / "src", cwd / "runtime", *site_dirs]

for dir in dirs:
    sys.path.append(str(dir))

# Fix zipimporter compatibility for packages imported from zip archives
# Some packages (e.g., markdown) require exec_module on zipimporter
import zipimport
if not hasattr(zipimport.zipimporter, 'exec_module'):
    def _create_module(self, spec):
        return None
    def _exec_module(self, module):
        code = self.get_code(module.__name__)
        exec(code, module.__dict__)
    zipimport.zipimporter.create_module = _create_module
    zipimport.zipimporter.exec_module = _exec_module

# Qt configuration (optional)
$QT_CONFIG

# Main entry point
from src.$PROJECT_NAME.$ENTRY_NAME import main
main()
"""


@dataclass(frozen=True)
class CompilerConfig:
    """Compiler configuration dataclass.

    Attributes:
        name: Compiler name
        args: Tuple of compiler arguments
    """

    name: str
    args: tuple[str, ...]

    def to_list(self) -> list[str]:
        """Convert arguments to list.

        Returns:
            List of compiler arguments
        """
        return list(self.args)


@dataclass(frozen=True)
class EntryFile:
    """Entry file dataclass with enhanced functionality."""

    project_name: str
    source_file: Path

    @cached_property
    def entry_name(self) -> str:
        """Get entry file name."""
        return self.source_file.stem

    @cached_property
    def module_name(self) -> str:
        """Get module name from source file stem."""
        return self.source_file.stem.replace("-", "_")

    @cached_property
    def is_gui(self) -> bool:
        """Determine if this entry file is a GUI application.

        Entry file is considered GUI if:
        1. File name contains 'gui' (e.g., docscan_gui.py, myapp-gui.py)
        2. File imports GUI frameworks (PySide2, PyQt5, PyQt6, tkinter)
        """
        # Check file name for 'gui' keyword
        if "gui" in self.source_file.stem.lower():
            return True

        # Check file content for GUI framework imports
        try:
            content = self.source_file.read_text(encoding="utf-8")
            gui_keywords = ["pyside2", "pyqt5", "pyqt6", "tkinter", "tkinter.ttk"]
            return any(f"from {kw}" in content.lower() or f"import {kw}" in content.lower()
                      for kw in gui_keywords)
        except Exception:
            return False

    def __repr__(self) -> str:
        return f"<EntryFile project_name={self.project_name} source_file={self.source_file} entry_name={self.entry_name} is_gui={self.is_gui}>"


# Compiler configurations
_COMPILER_CONFIGS: Final = [
    CompilerConfig(name="gcc", args=("-std=c99", "-Wall", "-O2")),
    CompilerConfig(name="clang", args=("-std=c99", "-Wall", "-O2")),
    CompilerConfig(name="cl", args=("/std:c99", "/O2")),
]


def find_compiler() -> str | None:
    """Find available C compiler."""
    for compiler in ("gcc", "clang", "cl"):
        if shutil.which(compiler):
            return compiler
    logger.error("No compiler found")
    return None


def get_compiler_args(compiler: str) -> list[str]:
    """Get compiler arguments for specified compiler.

    Args:
        compiler: Compiler name or path

    Returns:
        List of compiler arguments
    """
    compiler_name = (
        Path(compiler).stem if "\\" in compiler or "/" in compiler else compiler
    )

    for config in _COMPILER_CONFIGS:
        if config.name == compiler_name:
            return config.to_list()

    return []


def select_c_template(loader_type: str, debug: bool) -> str:
    """Select the appropriate C code template based on platform and type.

    In debug mode, always use console template to ensure output is visible.
    """
    if debug:
        loader_type = "console"

    if is_windows:
        return (
            _WINDOWS_GUI_TEMPLATE if loader_type == "gui" else _WINDOWS_CONSOLE_TEMPLATE
        )
    elif is_macos:
        return _MACOS_GUI_TEMPLATE if loader_type == "gui" else _MACOS_CONSOLE_TEMPLATE
    else:
        return _UNIX_GUI_TEMPLATE if loader_type == "gui" else _UNIX_CONSOLE_TEMPLATE


def prepare_entry_file(project: Project, entry_stem: str) -> str:
    """Generate entry file code by replacing placeholders in template.

    Args:
        project_name: Project name
        entry_stem: Entry file name without extension (e.g., "myapp" for "myapp.py" or "myapp.ent")
        is_qt: Whether this is a Qt application

    Returns:
        Generated entry file code
    """
    qt_config = (
        """# Qt configuration
qt_dir = cwd / "site-packages" / "{$QT_NAME}"
plugin_path = str(qt_dir / "plugins" / "platforms")
os.environ["QT_QPA_PLATFORM_PLUGIN_PATH"] = plugin_path

""".replace("$QT_NAME", project.qt_libname or "")
        if project.has_qt
        else ""
    )

    return (
        ENTRY_FILE_TEMPLATE
        .replace("$PROJECT_NAME", project.name.replace("-", "_"))
        .replace("$ENTRY_NAME", entry_stem)
        .replace("$QT_CONFIG", qt_config)
    )


def prepare_c_source(template: str, entry_file: str, is_debug: bool) -> str:
    """Generate C source code by replacing placeholders in template."""
    # Replace placeholders
    c_code = template.replace("${ENTRY_FILE}", entry_file)
    c_code = c_code.replace("${DEBUG_MODE}", "1" if is_debug else "0")
    return c_code


def compile_c_source(
    c_source_path: str | Path,
    output_filepath: Path,
    compiler: str | None = None,
) -> bool:
    """Compile C source code to executable file."""
    # Find compiler if not specified
    if compiler is None:
        compiler = find_compiler()
        if compiler is None:
            logger.error("Error: No suitable C compiler found (gcc/clang/cl required)")
            return False

    logger.debug(f"Using compiler: {compiler}")

    # Get compiler arguments
    compiler_args = get_compiler_args(compiler)

    # Prepare paths using pathlib
    c_source_path = Path(c_source_path)

    output_filepath.parent.mkdir(parents=True, exist_ok=True)
    output_filepath = output_filepath.with_suffix(ext)

    # Build compile command
    compiler_name = (
        Path(compiler).name if "\\" in compiler or "/" in compiler else compiler
    )
    if compiler_name.lower() == "cl" or compiler_name.lower() == "cl.exe":
        # MSVC compiler
        cmd = [
            compiler,
            *compiler_args,
            str(c_source_path),
            f"/Fe:{output_filepath}",
            "/link",
            "/SUBSYSTEM:WINDOWS"
            if "gui" in str(c_source_path).lower()
            else "/SUBSYSTEM:CONSOLE",
        ]
    else:
        # GCC/Clang compiler
        subsystem_flag = []
        # For Windows GUI, add -mwindows flag with gcc/clang
        if is_windows and "gui" in str(c_source_path).lower():
            subsystem_flag = ["-mwindows"]
        cmd = [
            compiler,
            *compiler_args,
            *subsystem_flag,
            "-o",
            str(output_filepath),
            str(c_source_path),
        ]

    logger.debug(f"Compiling with command: {' '.join(cmd)}")
    try:
        result = subprocess.run(cmd, capture_output=True, text=True)
        if result.returncode != 0:
            logger.debug(f"Compilation failed with return code {result.returncode}")
            logger.debug(f"STDOUT: {result.stdout}")
            logger.debug(f"STDERR: {result.stderr}")
            return False
        logger.info(f"Successfully compiled to: {output_filepath}")
        return True
    except FileNotFoundError:
        logger.error(f"Error: Compiler '{compiler}' not found")
        return False
    except Exception as e:
        logger.error(f"Error during compilation: {e}")
        return False


def _is_entry_file(file_path: Path) -> bool:
    """Check if a Python file is an entry file.

    An entry file must contain either:
    - def main() function
    - if __name__ == '__main__': block

    Args:
        file_path: Path to Python file

    Returns:
        True if file is an entry file, False otherwise
    """
    if not file_path.is_file():
        return False

    try:
        content = file_path.read_text(encoding="utf-8")
        return (
            "def main(" in content
            or "if __name__ == '__main__':" in content
            or 'if __name__ == "__main__":' in content
        )
    except Exception as e:
        logger.debug(f"Failed to read {file_path}: {e}")
        return False


def _detect_entry_files(
    project_dir: Path,
    project_name: str,
) -> list[EntryFile]:
    """Detect all entry files in project directory.

    Args:
        project_dir: Project directory
        project_name: Project name

    Returns:
        List of tuples (entry_file_name, module_name, is_gui)
        e.g., [("docscan", "docscan", False), ("docscan-gui", "docscan_gui", True)]
    """
    normalized_name = project_name.replace("-", "_")
    entry_files: list[EntryFile] = []

    # Scan all Python files in project directory
    for py_file in project_dir.glob("*.py"):
        if not _is_entry_file(py_file):
            continue

        entry_file = EntryFile(project_name=normalized_name, source_file=py_file)
        entry_files.append(entry_file)
        logger.debug(f"Found entry file: {entry_file}")

    return entry_files


@dataclass
class PyLoaderBuilder:
    """Helper class to build individual loaders."""

    parent: PyLoaderGenerator
    project: Project
    entry_file: EntryFile
    is_debug: bool = False

    @cached_property
    def build_dir(self) -> Path:
        """Build directory path."""
        return self.parent.root_dir / _DEFAULT_BUILD_DIR

    @cached_property
    def output_dir(self) -> Path:
        """Output directory path."""
        return self.parent.root_dir / _DEFAULT_OUTPUT_DIR

    @cached_property
    def output_exe(self) -> Path:
        """Output executable path."""
        return self.output_dir / f"{self.entry_file.entry_name}{ext}"

    @cached_property
    def loader_type(self) -> str:
        """Determine loader type based on entry file.

        Entry file is GUI if:
        - The entry file itself is detected as GUI (contains 'gui' in name or imports GUI libs)
        - Otherwise, fall back to project-level loader_type
        """
        return "gui" if self.entry_file.is_gui else self.project.loader_type

    @cached_property
    def c_source_path(self) -> Path:
        """C source file path."""
        filename = (
            f"{self.entry_file.entry_name}_debug_console.c"
            if self.is_debug
            else f"{self.entry_file.entry_name}_{self.loader_type}.c"
        )
        return self.build_dir / filename

    @cached_property
    def entry_filepath(self) -> Path:
        """Entry file path."""
        return self.output_dir / f"{self.entry_file.entry_name}.ent"

    def generate(self) -> bool:
        """Generate loader for a single entry file."""
        t0 = time.perf_counter()

        # Prepare directories
        self.build_dir.mkdir(parents=True, exist_ok=True)
        self.output_dir.mkdir(parents=True, exist_ok=True)

        # Generate entry file
        entry_file_code = prepare_entry_file(
            project=self.project, entry_stem=self.entry_file.entry_name
        )
        self.entry_filepath.write_text(entry_file_code, encoding="utf-8")
        logger.debug(f"Generated entry file: {self.entry_filepath}")

        # Generate and compile C source
        c_template = select_c_template(self.loader_type, self.is_debug)
        c_code = prepare_c_source(c_template, self.entry_filepath.name, self.is_debug)
        self.c_source_path.write_text(c_code, encoding="utf-8")
        logger.debug(f"Generated C source code: {self.c_source_path}")

        compiler = find_compiler()
        if not compiler:
            logger.error(f"Failed to find compiler: {compiler}")
            return False

        success = compile_c_source(self.c_source_path, self.output_exe, compiler)
        if success:
            logger.info(f"Loader executable generated successfully: {self.output_exe}")
            logger.debug(f"Entry: {self.entry_file}")
            logger.debug(f"Type: {self.loader_type}")
            logger.debug(f"Qt: {self.project.has_qt}")
            logger.debug(f"Debug mode: {self.is_debug}")
            logger.debug(f"Compilation time: {time.perf_counter() - t0:.4f} seconds")
        else:
            logger.error(f"Failed to compile loader for {self.entry_file.entry_name}")

        return success


@dataclass
class PyLoaderGenerator:
    """Main class for generating Python loader executables."""

    root_dir: Path
    build_dir: Path = field(default_factory=lambda: _DEFAULT_BUILD_DIR)
    output_dir: Path = field(default_factory=lambda: _DEFAULT_OUTPUT_DIR)
    compiler: str | None = None

    @cached_property
    def solution(self) -> Solution:
        """Get the solution from the target directory."""
        return Solution.from_directory(self.root_dir)

    def generate_for_project(
        self, project: Project, project_dir: Path, debug: bool = False
    ) -> bool:
        """Generate loader executables and entry files for a single project.

        This function detects all entry files (main and GUI versions) and generates
        a loader for each one.

        Args:
            project: Project information dataclass
            project_dir: Directory containing the project (pyproject.toml location)
            debug: Whether to generate debug version
        """
        # Detect all entry files in project directory
        entry_files = _detect_entry_files(project_dir, project.name)
        if not entry_files:
            logger.error(f"No entry files found in {project_dir}")
            return False

        # Generate loaders for all entry files
        for entry_file in entry_files:
            logger.debug(f"Generating loader for: {entry_file.project_name}")
            builder = PyLoaderBuilder(
                parent=self, project=project, entry_file=entry_file, is_debug=debug
            )

            if not builder.generate():
                return False

        return True

    def run(self, debug: bool = False) -> None:
        """Generate loader executables for all projects concurrently."""
        projects = self.solution.projects
        if not projects:
            logger.error("Failed to load project information")
            return

        success_count = 0
        failed_projects: list[str] = []

        if len(projects) == 1:
            logger.debug("Only one project found, using current directory")
            project_dir = self.root_dir
            if self.generate_for_project(
                project=next(iter(projects.values())),
                project_dir=project_dir,
                debug=debug,
            ):
                success_count += 1
            else:
                failed_projects.append(next(iter(projects)))
        else:
            # Prepare project tasks
            project_tasks = []
            for project_name, project in projects.items():
                project_dir = self.root_dir / project_name
                if not project_dir.is_dir():
                    logger.error(
                        f"Project directory not found: {project_dir}, skipping..."
                    )
                    failed_projects.append(project_name)
                    continue
                project_tasks.append((project_name, project, project_dir))

            # Execute compilation tasks concurrently
            logger.info(f"Compiling {len(project_tasks)} projects concurrently...")
            with ThreadPoolExecutor(max_workers=None) as executor:
                future_to_project = {
                    executor.submit(
                        self.generate_for_project, project, project_dir, debug
                    ): project_name
                    for project_name, project, project_dir in project_tasks
                }

                for future in as_completed(future_to_project):
                    project_name = future_to_project[future]
                    try:
                        if future.result():
                            success_count += 1
                        else:
                            failed_projects.append(project_name)
                    except Exception as e:
                        logger.error(f"Project {project_name} compilation failed: {e}")
                        failed_projects.append(project_name)

        logger.info(f"Pack {success_count}/{len(projects)} projects successfully")

        if failed_projects:
            logger.error(f"Failed: {failed_projects}")


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Generate Python loader executables for all projects in directory"
    )
    parser.add_argument(
        "directory",
        nargs="?",
        default=str(cwd),
        help="Directory containing projects (will create/load projects.json)",
    )
    parser.add_argument("--debug", "-d", action="store_true", help="Enable debug mode")
    parser.add_argument(
        "--compiler",
        help="Specify compiler to use. Examples: gcc, clang, cl, or full path like C:\\vc\\bin\\cl.exe",
    )
    return parser.parse_args()


def main():
    """Main entry point for pyloadergen."""
    args = parse_args()

    if args.debug:
        logger.setLevel(logging.DEBUG)

    working_dir = Path(args.directory)
    if not working_dir.is_dir():
        logger.error(f"Directory does not exist: {working_dir}")
        return

    logger.info(f"Working directory: {working_dir}")

    generator = PyLoaderGenerator(root_dir=working_dir, compiler=args.compiler)
    generator.run(debug=args.debug)


if __name__ == "__main__":
    main()
