from __future__ import annotations

import argparse
import json
import logging
import platform
import shutil
import subprocess
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

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="%(levelname)s: %(message)s")
logger = logging.getLogger(__name__)
cwd = Path.cwd()


@dataclass(frozen=True)
class ProjectInfo:
    """Project information dataclass.

    Attributes:
        name: Project name
        version: Project version
        description: Project description
        dependencies: List of project dependencies
        keywords: Project keywords
        scripts: Entry point scripts
    """

    name: str
    version: str = "0.0.0"
    description: str = ""
    dependencies: list[str] = field(default_factory=list)
    keywords: list[str] = field(default_factory=list)
    scripts: dict[str, str] = field(default_factory=dict)

    @classmethod
    def from_dict(cls, project_name: str, data: dict[str, Any]) -> ProjectInfo:
        """Create ProjectInfo from dictionary.

        Args:
            project_name: Project name
            data: Dictionary containing project information

        Returns:
            ProjectInfo instance
        """
        return cls(
            name=project_name,
            version=data.get("version", "0.0.0"),
            description=data.get("description", ""),
            dependencies=data.get("dependencies", []),
            keywords=data.get("keywords", []),
            scripts=data.get("scripts", {}),
        )

    def to_dict(self) -> dict[str, Any]:
        """Convert ProjectInfo to dictionary.

        Returns:
            Dictionary representation of ProjectInfo
        """
        return {
            "name": self.name,
            "version": self.version,
            "description": self.description,
            "dependencies": self.dependencies,
            "keywords": self.keywords,
            "scripts": self.scripts,
        }


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

    Attributes:
        name: Compiler name
        args: List 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)


_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
    // GUI non-debug mode uses pythonw.exe (no console), other cases use python.exe
    if (is_debug) {
        // Debug mode: use python.exe to show console output
        snprintf(python_runtime, MAX_PATH_LEN, "%s\\runtime\\python.exe", exe_dir);
    } else {
        // Production GUI mode: use pythonw.exe without creating console window
        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 (add -u parameter for real-time output capture)
    if (is_debug) {
        // Debug mode: do not redirect output, display output on console
        if (lpCmdLine && strlen(lpCmdLine) > 0) {
            snprintf(cmd, MAX_PATH_LEN, "\"%s\" -u \"%s\" %s", python_runtime, script_path, lpCmdLine);
        } else {
            snprintf(cmd, MAX_PATH_LEN, "\"%s\" -u \"%s\"", python_runtime, script_path);
        }
    } else {
        // Production mode: redirect all output to pipe
        if (lpCmdLine && strlen(lpCmdLine) > 0) {
            snprintf(cmd, MAX_PATH_LEN, "\"%s\" -u \"%s\" %s 2>&1", python_runtime, script_path, lpCmdLine);
        } else {
            snprintf(cmd, MAX_PATH_LEN, "\"%s\" -u \"%s\" 2>&1", python_runtime, script_path);
        }
    }
}

// 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] = "";
    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 and execute Python command
    build_python_command(cmd, exe_dir, "${ENTRY_FILE}", ${DEBUG_MODE}, lpCmdLine);

    // Create pipe for capturing output
    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;
    }

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

    // Set different startup methods based on debug mode
    if (${DEBUG_MODE}) {
        // Debug mode: do not use pipe, inherit console to display output
        // Keep si.dwFlags as 0, do not set STARTF_USESTDHANDLES
    } else {
        // Production GUI mode: use pipe to capture error output
        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, FALSE,
        createFlags,
        NULL, exe_dir, &si, &pi
    );

    if (!success) {
        DWORD error = GetLastError();
        char error_msg[MAX_ERROR_LEN];
        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);

    // Debug output
    if (${DEBUG_MODE}) {
#ifdef _WIN32
        fwprintf(stderr, L"DEBUG: Python process exited with code: %lu\n", exit_code);
#else
        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];

    // 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);

    // Build command line (add -u parameter for real-time output capture)
    // Include original command line arguments
    snprintf(cmd, MAX_PATH_LEN, "\"%s\" -u \"%s\"", python_runtime, script_path);

    // Append original command line arguments if any
    if (argc > 1) {
        strcat(cmd, " ");
        for (int i = 1; i < argc; i++) {
            strcat(cmd, "\"");
            strcat(cmd, argv[i]);
            strcat(cmd, "\"");
            if (i < argc - 1) {
                strcat(cmd, " ");
            }
        }
    }
}

// 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;

    // Set encoding to UTF-8
    setup_encoding();

    // 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)) {
        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 Python command
    build_python_command(cmd, exe_dir, "${ENTRY_FILE}", argc, argv);

    // Debug output
    if (${DEBUG_MODE}) {
#ifdef _WIN32
        // On Windows, convert to wide characters for proper Unicode display
        {
            // Convert and print command
            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);
            }

            // Convert and print exe_dir
            int wdir_len = MultiByteToWideChar(CP_UTF8, 0, exe_dir, -1, NULL, 0);
            if (wdir_len > 0) {
                wchar_t* wdir = (wchar_t*)malloc(wdir_len * sizeof(wchar_t));
                MultiByteToWideChar(CP_UTF8, 0, exe_dir, -1, wdir, wdir_len);
                fwprintf(stderr, L"DEBUG: exe_dir: %ls\n", wdir);
                free(wdir);
            }
        }
#else
        fprintf(stderr, "DEBUG: Command to execute: %s\n", cmd);
        fprintf(stderr, "DEBUG: exe_dir: %s\n", exe_dir);
#endif
    }

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

    // For console applications, always inherit console handles to display output directly
    // Don't use pipes in non-debug mode to allow all output to display in console
    if (${DEBUG_MODE}) {
        // Debug mode: do not use pipe, inherit console to display output directly
        // Keep si.dwFlags as 0, do not set STARTF_USESTDHANDLES
    } else {
        // Production mode for console apps: inherit console handles to show ALL output in console
        // Do not use pipes at all to ensure all stdout/stderr displays in console
    }

    // 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);

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

    // 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
) {
    char script_path[MAX_PATH_LEN];

    // Build startup script path
    snprintf(script_path, MAX_PATH_LEN * 2, "%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
    snprintf(cmd, MAX_PATH_LEN * 2,
        "cd \"%s\" && nohup python3 -u \"%s\" >\"%s\" 2>&1 &",
        exe_dir, script_path, log_file
    );
}

// Unix GUI entry point
int main(int argc, char* argv[]) {
    (void)argc;  // Unused parameter (argv[0] is used)
    char exe_dir[MAX_PATH_LEN];
    char cmd[MAX_PATH_LEN * 2];
    char log_file[MAX_PATH_LEN];
    pid_t pid;
    int status;

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

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

    // Build and execute Python command
    build_python_command(cmd, exe_dir, "${ENTRY_FILE}");
    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
) {
    char script_path[MAX_PATH_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
    snprintf(cmd, MAX_PATH_LEN * 2,
        "cd \"%s\" && python3 -u \"%s\"",
        exe_dir, script_path
    );
}

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

    // 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
    char* last_slash = strrchr(exe_dir, '/');
    if (last_slash) {
        *last_slash = '\0';
    }

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

    // 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, use argv[0]
        realpath("./", exe_dir);
    }

    // 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
) {
    char python_runtime[MAX_PATH_LEN];
    char script_path[MAX_PATH_LEN];
    char log_file[MAX_PATH_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)
    snprintf(cmd, MAX_PATH_LEN * 2,
        "cd \"%s\" && nohup \"%s\" -u \"%s\" >\"%s\" 2>&1 &",
        exe_dir, python_runtime, script_path, log_file
    );
}

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

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

    // Check Python runtime
    char python_runtime[MAX_PATH_LEN];
    snprintf(python_runtime, MAX_PATH_LEN, "%s/runtime/bin/python3", exe_dir);
    if (!check_python_runtime(python_runtime)) {
        // macOS GUI app needs to use dialog to display error
        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}");
    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>

#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 {
        realpath("./", exe_dir);
    }
}

// Build Python command line
static void build_python_command(
    char* cmd,
    const char* exe_dir,
    const char* entry_file
) {
    char python_runtime[MAX_PATH_LEN];
    char script_path[MAX_PATH_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)
    snprintf(cmd, MAX_PATH_LEN,
        "cd \"%s\" && \"%s\" -u \"%s\"",
        exe_dir, python_runtime, script_path
    );
}

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

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

    // Check Python runtime
    char python_runtime[MAX_PATH_LEN];
    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}");

    // 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_FILE import main
main()
"""

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

# Qt-related keywords and dependencies for faster detection
_QT_DEPENDENCIES: frozenset[str] = frozenset((
    "qt",
    "pyside",
    "pyqt",
    "pyside2",
    "pyside6",
    "pyqt5",
    "pyqt6",
    "qt5",
    "qt6",
))
_GUI_KEYWORDS: frozenset[str] = frozenset(("gui", "desktop"))


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,
    is_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.
    """
    # In debug mode, always use console template for visibility
    if is_debug:
        if is_windows:
            return _WINDOWS_CONSOLE_TEMPLATE
        elif is_macos:
            return _MACOS_CONSOLE_TEMPLATE
        else:
            return _UNIX_CONSOLE_TEMPLATE

    # In non-debug mode, use the requested template type
    if is_windows:
        if loader_type == "gui":
            return _WINDOWS_GUI_TEMPLATE
        else:
            return _WINDOWS_CONSOLE_TEMPLATE
    elif is_macos:
        if loader_type == "gui":
            return _MACOS_GUI_TEMPLATE
        else:
            return _MACOS_CONSOLE_TEMPLATE
    else:  # Linux and other Unix-like systems
        if loader_type == "gui":
            return _UNIX_GUI_TEMPLATE
        else:
            return _UNIX_CONSOLE_TEMPLATE


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

    Args:
        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
    """
    entry_file_code = ENTRY_FILE_TEMPLATE.replace("$PROJECT_NAME", entry_stem)

    # Add Qt configuration if needed
    if is_qt:
        qt_config = """# Qt configuration
qt_dir = cwd / "site-packages" / "PySide2"
plugin_path = str(qt_dir / "plugins" / "platforms")
os.environ["QT_QPA_PLATFORM_PLUGIN_PATH"] = plugin_path

"""
    else:
        qt_config = ""

    return entry_file_code.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 ensure_projects_json(directory: Path) -> Path | None:
    """Ensure projects.json exists by running projectparse if needed.

    Args:
        directory: Directory to check for projects.json

    Returns:
        Path to projects.json if successful, None otherwise
    """
    projects_json = directory / "projects.json"
    if projects_json.exists():
        logger.debug(f"Found existing projects.json at {projects_json}")
        return projects_json

    logger.info("projects.json not found, running projectparse...")

    try:
        import sys

        from sfi.projectparse import projectparse as pp

        original_argv = sys.argv.copy()
        sys.argv = ["projectparse", "--directory", str(directory), "--output", "projects.json", "--recursive"]
        try:
            pp.main()
        finally:
            sys.argv = original_argv

        return projects_json if projects_json.exists() else None
    except ImportError:
        sfi_dir = Path(__file__).parent.parent.parent
        projectparse_script = sfi_dir / "projectparse" / "projectparse.py"

        if not projectparse_script.exists():
            logger.error(f"Cannot find projectparse script at {projectparse_script}")
            return None

        result = subprocess.run(
            [
                "python",
                str(projectparse_script),
                "--directory",
                str(directory),
                "--output",
                "projects.json",
                "--recursive",
            ],
            capture_output=True,
            text=True,
            cwd=directory,
        )
        if result.returncode == 0 and projects_json.exists():
            logger.info("projectparse completed successfully")
            return projects_json
        logger.error(f"projectparse failed: {result.stderr}")
        return None
    except Exception as e:
        logger.error(f"Failed to run projectparse: {e}")
        return None


def load_projects_json(projects_json: Path) -> dict | None:
    """Load project information from projects.json.

    Args:
        projects_json: Path to projects.json file

    Returns:
        Dictionary of project information, None if failed
    """
    try:
        with open(projects_json, encoding="utf-8") as f:
            return json.load(f)
    except Exception as e:
        logger.error(f"Failed to load projects.json: {e}")
        return None


def _detect_loader_type(project_info: ProjectInfo) -> tuple[str, bool]:
    """Detect loader type and Qt usage from project info.

    Args:
        project_info: Project information dataclass

    Returns:
        Tuple of (loader_type: str, use_qt: bool)
    """
    # Convert dependencies and keywords to frozenset for O(1) lookup
    deps_lower = frozenset(dep.lower() for dep in project_info.dependencies)
    keywords_lower = frozenset(kw.lower() for kw in project_info.keywords)

    # Check for Qt dependencies using set intersection (O(min(n,m)) instead of O(n*m))
    use_qt = bool(deps_lower.intersection(_QT_DEPENDENCIES))

    # Determine loader type: GUI if has Qt dependencies or GUI keywords
    loader_type = "gui" if use_qt or bool(keywords_lower.intersection(_GUI_KEYWORDS)) else "console"
    return loader_type, use_qt


def _get_entry_module(project_info: ProjectInfo) -> str:
    """Get entry module from project info.

    Args:
        project_info: Project information dataclass

    Returns:
        Entry module name
    """
    if not project_info.scripts:
        return project_info.name.replace("-", "_")
    first_script = next(iter(project_info.scripts.values()))
    return first_script.split(":")[0].strip()


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.exists() or not file_path.is_file():
        return False

    try:
        content = file_path.read_text(encoding="utf-8")
        # Check for def main() or def main(
        has_main_func = "def main(" in content or "def main():" in content
        # Check for if __name__ == '__main__':
        has_main_block = "if __name__ == '__main__':" in content or 'if __name__ == "__main__":' in content
        return has_main_func or has_main_block
    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, loader_type: str) -> list[tuple[str, str, bool]]:
    """Detect all entry files in project directory.

    Args:
        project_dir: Project directory
        project_name: Project name
        loader_type: Base loader type from project info

    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 = []

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

        # Get file stem (name without .py extension)
        file_stem = py_file.stem

        # Determine if this is a GUI entry based on file name
        is_gui_file = file_stem.endswith("_gui")

        # Generate entry name (use hyphen for GUI version)
        if is_gui_file:
            # e.g., "docscan_gui" -> "docscan-gui"
            base_name = file_stem.replace("_gui", "")
            entry_name = f"{base_name}-gui"
        else:
            entry_name = file_stem

        # Module name is just the file stem
        module_name = file_stem

        entry_files.append((entry_name, module_name, is_gui_file))
        logger.debug(f"Found entry file: {py_file.name} -> {entry_name} (GUI: {is_gui_file})")

    # If no entry files found, fallback to default
    if not entry_files:
        logger.warning(f"No entry files found in {project_dir}, using default: {normalized_name}.py")
        entry_files.append((project_name, normalized_name, False))

    return entry_files


def generate_loader(
    project_name: str,
    projects_json_path: Path,
    is_debug: bool = False,
    compiler: str | None = None,
) -> bool:
    """Generate loader executable and entry file from projects.json.

    Args:
        project_name: Name of the project to generate loader for
        projects_json_path: Path to projects.json file
        is_debug: Whether to generate debug version
        compiler: Optional compiler to use

    Returns:
        True if successful, False otherwise
    """
    # Load projects.json
    projects_data = load_projects_json(projects_json_path)
    if not projects_data:
        logger.error("Failed to load projects.json")
        return False

    # Check if project exists
    if project_name not in projects_data:
        logger.error(f"Project '{project_name}' not found in projects.json")
        return False

    # Convert to ProjectInfo
    project_info = ProjectInfo.from_dict(project_name, projects_data[project_name])
    project_dir = projects_json_path.parent

    return generate_loader_for_project(project_dir, project_info, is_debug, compiler)


def _generate_single_loader(
    project_dir: Path,
    entry_name: str,
    entry_module: str,
    loader_type: str,
    use_qt: bool,
    is_debug: bool,
    compiler: str | None,
) -> bool:
    """Generate a single loader executable and entry file.

    Args:
        project_dir: Project directory
        entry_name: Entry file name (e.g., "docscan", "docscan-gui")
        entry_module: Python module name (e.g., "docscan", "docscan_gui")
        loader_type: Loader type ("console" or "gui")
        use_qt: Whether this is a Qt application
        is_debug: Whether to generate debug version
        compiler: Optional compiler to use

    Returns:
        True if successful, False otherwise
    """
    # Prepare directories
    build_dir = project_dir / ".cbuild"
    output_dir = project_dir / "dist"
    build_dir.mkdir(parents=True, exist_ok=True)
    output_dir.mkdir(parents=True, exist_ok=True)

    # Output paths
    output_exe = output_dir / f"{entry_name}{ext}"
    c_source_filename = f"{entry_name}_debug_console.c" if is_debug else f"{entry_name}_{loader_type}.c"
    entry_filepath = output_dir / f"{entry_name}.ent"
    c_source_path = build_dir / c_source_filename

    t0 = time.perf_counter()

    # Generate entry file
    # Extract project name from entry_name (remove -gui suffix if present)
    project_name = entry_name.replace("-gui", "")
    entry_file_code = prepare_entry_file(project_name, use_qt)
    entry_file_code = entry_file_code.replace("$ENTRY_FILE", entry_module)
    entry_filepath.write_text(entry_file_code, encoding="utf-8")
    logger.info(f"Generated entry file: {entry_filepath}")

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

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

    return success


def generate_loader_for_project(
    project_dir: Path,
    project_info: ProjectInfo,
    is_debug: bool = False,
    compiler: str | None = None,
) -> 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_dir: Directory containing the project (pyproject.toml location)
        project_info: Project information dataclass
        is_debug: Whether to generate debug version
        compiler: Optional compiler to use

    Returns:
        True if all loaders generated successfully, False otherwise
    """
    logger.info(f"\n{'=' * 60}")
    logger.info(f"Generating loaders for project: {project_info.name}")
    logger.info(f"{'=' * 60}")

    # Determine base loader type and Qt usage from project info
    base_loader_type, use_qt = _detect_loader_type(project_info)

    # Detect all entry files in project directory
    entry_files = _detect_entry_files(project_dir, project_info.name, base_loader_type)
    logger.info(f"Found {len(entry_files)} entry file(s): {[e[0] for e in entry_files]}")

    # Generate loader for each entry file
    all_success = True
    for entry_name, entry_module, is_gui in entry_files:
        logger.info(f"\n--- Generating loader for: {entry_name} ---")

        # Use GUI loader type for GUI entries, otherwise use base type
        loader_type = "gui" if is_gui else base_loader_type
        logger.debug(f"Loader type: {loader_type}")
        logger.debug(f"Use Qt: {use_qt}")
        logger.debug(f"Entry module: {entry_module}")

        success = _generate_single_loader(
            project_dir=project_dir,
            entry_name=entry_name,
            entry_module=entry_module,
            loader_type=loader_type,
            use_qt=use_qt,
            is_debug=is_debug,
            compiler=compiler,
        )

        if not success:
            all_success = False
            logger.error(f"Failed to generate loader for {entry_name}")

    return all_success


def main() -> int:
    """Main entry point for pyloadergen.

    Returns:
        Exit code (0 for success, 1 for failure)
    """
    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"
    )
    args = parser.parse_args()

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

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

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

    # Ensure projects.json exists and load projects
    projects_json = ensure_projects_json(base_dir)
    if not projects_json:
        logger.error("Failed to create projects.json")
        return 1

    projects_data = load_projects_json(projects_json)
    if not projects_data:
        logger.error("Failed to load project information")
        return 1

    # Convert dict to ProjectInfo dataclasses
    projects: dict[str, ProjectInfo] = {name: ProjectInfo.from_dict(name, data) for name, data in projects_data.items()}

    logger.info(f"Found {len(projects)} project(s) to process")

    # Generate loaders
    total_start = time.perf_counter()
    success_count = 0
    failed_projects: list[str] = []
    use_current_dir = len(projects) == 1

    if use_current_dir:
        logger.debug("Only one project found, using current directory")

    for project_name, project_info in projects.items():
        project_dir = base_dir if use_current_dir else base_dir / project_name
        if not project_dir.is_dir():
            logger.warning(f"Project directory not found: {project_dir}, skipping")
            failed_projects.append(project_name)
            continue

        if generate_loader_for_project(project_dir, project_info, args.debug, args.compiler):
            success_count += 1
        else:
            failed_projects.append(project_name)

    total_time = time.perf_counter() - total_start

    # Print summary
    logger.info(f"\n{'=' * 60}")
    logger.info("Summary")
    logger.info(f"{'=' * 60}")
    logger.info(f"Total projects: {len(projects)}")
    logger.info(f"Successfully generated: {success_count}")
    logger.info(f"Failed: {len(failed_projects)}")
    if failed_projects:
        logger.info(f"Failed projects: {', '.join(failed_projects)}")
    logger.info(f"Total time: {total_time:.4f} seconds")

    return 0 if not failed_projects else 1


if __name__ == "__main__":
    import sys

    sys.exit(main())
