"""
Compilation engine for luainstaller.
https://github.com/Water-Run/luainstaller

This module provides the core compilation functionality using luastatic
to build standalone executables from Lua scripts.

:author: WaterRun
:file: engine.py
:date: 2025-12-05
"""

import os
import shutil
import subprocess
from pathlib import Path
from typing import TYPE_CHECKING

from .exceptions import (
    CompilationFailedError,
    CompilerNotFoundError,
    LuastaticNotFoundError,
    OutputFileNotFoundError,
    ScriptNotFoundError,
)

if TYPE_CHECKING:
    from collections.abc import Sequence


def verify_environment() -> None:
    """
    Verify that required tools are available in PATH.
    
    :raises LuastaticNotFoundError: If luastatic is not installed
    :raises CompilerNotFoundError: If gcc is not available
    """
    if not shutil.which("luastatic"):
        raise LuastaticNotFoundError()
    
    if not shutil.which("gcc"):
        raise CompilerNotFoundError("gcc")


def _find_lua_library() -> str | None:
    """
    Find the Lua shared library path based on the Lua interpreter in PATH.
    
    :return: Path to Lua library, or None if not found
    """
    lua_executable = shutil.which("lua")
    if not lua_executable:
        return None
    
    lua_path = Path(lua_executable).resolve()
    
    try:
        result = subprocess.run(
            [str(lua_path), "-v"],
            capture_output=True,
            text=True,
            timeout=5,
            check=False
        )
        version_output = result.stdout.strip() or result.stderr.strip()
    except (subprocess.TimeoutExpired, OSError):
        version_output = ""
    
    version_suffix = ""
    for ver in ("5.4", "5.3", "5.2", "5.1"):
        if f"Lua {ver}" in version_output:
            version_suffix = ver
            break
    
    bin_dir = lua_path.parent
    prefix_dir = bin_dir.parent
    
    candidate_dirs = [
        prefix_dir / "lib64",
        prefix_dir / "lib",
        prefix_dir / "lib" / "x86_64-linux-gnu",
        prefix_dir / "lib" / "aarch64-linux-gnu",
        prefix_dir / "lib" / "i386-linux-gnu",
        bin_dir,
        Path("/usr/lib64"),
        Path("/usr/lib"),
        Path("/usr/local/lib64"),
        Path("/usr/local/lib"),
        Path("/usr/lib/x86_64-linux-gnu"),
        Path("/usr/lib/aarch64-linux-gnu"),
    ]
    
    if os.name == "nt":
        candidate_dirs.extend([bin_dir, prefix_dir, prefix_dir / "bin"])
    
    lib_names: list[str] = []
    if version_suffix:
        if os.name == "nt":
            lib_names.extend([
                f'lua{version_suffix.replace(".", "")}.dll',
                f"lua{version_suffix}.dll",
                "lua.dll",
            ])
        else:
            lib_names.extend([
                f"liblua{version_suffix}.so",
                f"liblua-{version_suffix}.so",
                f"liblua{version_suffix}.a",
                "liblua.so",
                "liblua.a",
            ])
    else:
        if os.name == "nt":
            lib_names.extend(["lua54.dll", "lua53.dll", "lua52.dll", "lua51.dll", "lua.dll"])
        else:
            lib_names.extend([
                "liblua5.4.so", "liblua5.3.so", "liblua5.2.so", "liblua5.1.so",
                "liblua-5.4.so", "liblua-5.3.so", "liblua-5.2.so", "liblua-5.1.so",
                "liblua.so",
                "liblua5.4.a", "liblua5.3.a", "liblua5.2.a", "liblua5.1.a",
                "liblua.a",
            ])
    
    for candidate_dir in candidate_dirs:
        if not candidate_dir.exists():
            continue
        for lib_name in lib_names:
            lib_path = candidate_dir / lib_name
            if lib_path.exists():
                return str(lib_path.resolve())
    
    try:
        pkg_names = (
            [f"lua{version_suffix}", f"lua-{version_suffix}", "lua"]
            if version_suffix
            else ["lua5.4", "lua5.3", "lua"]
        )
        for pkg_name in pkg_names:
            result = subprocess.run(
                ["pkg-config", "--variable=libdir", pkg_name],
                capture_output=True,
                text=True,
                timeout=5,
                check=False
            )
            if result.returncode == 0 and result.stdout.strip():
                libdir = Path(result.stdout.strip())
                if libdir.exists():
                    for lib_name in lib_names:
                        lib_path = libdir / lib_name
                        if lib_path.exists():
                            return str(lib_path.resolve())
    except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
        ...
    
    return None


def _cleanup_temp_files(entry_path: Path, output_dir: Path) -> list[str]:
    """
    Clean up temporary .c files generated by luastatic.
    
    :param entry_path: Path to the entry script
    :param output_dir: Directory where compilation was performed
    :return: List of deleted file paths
    """
    deleted: list[str] = []
    entry_name = entry_path.stem
    
    patterns = [
        f"{entry_name}.luastatic.c",
        f"{entry_name}.lua.c",
        f"{entry_name}.c",
    ]
    
    for pattern in patterns:
        temp_file = output_dir / pattern
        if temp_file.exists():
            try:
                temp_file.unlink()
                deleted.append(str(temp_file))
            except OSError:
                ...
    
    for c_file in output_dir.glob("*.luastatic.c"):
        if str(c_file) not in deleted:
            try:
                c_file.unlink()
                deleted.append(str(c_file))
            except OSError:
                ...
    
    return deleted


def compile_lua_script(
    entry_script: str,
    dependencies: Sequence[str],
    output: str | None = None,
    verbose: bool = False,
) -> str:
    """
    Compile Lua script with dependencies into standalone executable.
    
    :param entry_script: Path to entry Lua script
    :param dependencies: List of dependency file paths
    :param output: Output executable path (optional)
    :param verbose: Enable verbose output
    :return: Path to generated executable
    """
    verify_environment()
    
    entry_path = Path(entry_script).resolve()
    if not entry_path.exists():
        raise ScriptNotFoundError(str(entry_script))
    
    if output:
        output_path = Path(output).resolve()
        output_dir = output_path.parent
    else:
        output_dir = Path.cwd()
        output_name = entry_path.stem + (".exe" if os.name == "nt" else "")
        output_path = output_dir / output_name
    
    output_dir.mkdir(parents=True, exist_ok=True)
    
    command = ["luastatic", str(entry_path)]
    
    for dep in dependencies:
        dep_path = Path(dep).resolve()
        if not dep_path.exists():
            if verbose:
                print(f"Warning: Dependency not found: {dep}")
            continue
        command.append(str(dep_path))
    
    if lua_lib := _find_lua_library():
        command.append(lua_lib)
        if verbose:
            print(f"Using Lua library: {lua_lib}")
    else:
        if verbose:
            print("Warning: Lua library not found, luastatic may fail")
    
    command.extend(["-o", str(output_path)])
    
    if verbose:
        print(f"Executing: {' '.join(command)}")
        print(f"Working directory: {output_dir}")
    
    result = subprocess.run(
        command,
        cwd=str(output_dir),
        capture_output=True,
        text=True,
    )
    
    _cleanup_temp_files(entry_path, output_dir)
    
    if result.returncode != 0:
        raise CompilationFailedError(
            " ".join(command),
            result.returncode,
            result.stderr,
        )
    
    if verbose and result.stdout:
        print(result.stdout)
    
    if not output_path.exists():
        raise OutputFileNotFoundError(str(output_path))
    
    if verbose:
        print(f"Compilation successful: {output_path}")
    
    return str(output_path)


def get_environment_status() -> dict[str, bool]:
    """
    Get status of compilation environment.
    
    :return: Dictionary with tool availability status
    """
    return {
        "luastatic": bool(shutil.which("luastatic")),
        "gcc": bool(shutil.which("gcc")),
        "lua": bool(shutil.which("lua")),
        "lua_library": bool(_find_lua_library()),
    }


def print_environment_status() -> None:
    """Print compilation environment status."""
    status = get_environment_status()
    
    print("Compilation Environment Status:")
    print("=" * 50)
    
    for tool, available in status.items():
        symbol = "✓" if available else "✗"
        print(f"{symbol} {tool}")
    
    if lua_lib := _find_lua_library():
        print(f"  Path: {lua_lib}")
    
    print("=" * 50)
    
    if not status["luastatic"]:
        print("\nInstall luastatic:")
        print("  luarocks install luastatic")
    
    if not status["gcc"]:
        print("\nInstall gcc:")
        print("  Ubuntu/Debian: sudo apt install build-essential")
        print("  Fedora/RHEL:   sudo dnf install gcc")
        print("  Windows:       https://github.com/niXman/mingw-builds-binaries")
    
    if not status["lua"]:
        print("\nInstall Lua:")
        print("  Ubuntu/Debian: sudo apt install lua5.4")
        print("  Fedora/RHEL:   sudo dnf install lua")
        print("  Windows:       https://www.lua.org/download.html")
    
    if not status["lua_library"]:
        print("\nLua library not found. Install Lua development files:")
        print("  Ubuntu/Debian: sudo apt install liblua5.4-dev")
        print("  Fedora/RHEL:   sudo dnf install lua-devel")