"""
Core functionality for npm2rez - A tool to convert Node.js packages to rez packages
"""

import json
import os
import shutil
import subprocess


def create_package(args):
    """Create rez package"""
    # Convert package name to rez compatible format (use underscore instead of hyphen)
    rez_name = convert_name_to_rez_format(args.name)

    # Create output directory
    output_dir = os.path.abspath(args.output)
    package_dir = os.path.join(output_dir, rez_name, args.version)
    os.makedirs(package_dir, exist_ok=True)

    # Create package.py file
    create_package_py(args, package_dir)

    # Install Node.js package
    install_node_package(args, package_dir)

    return package_dir


def create_package_py(args, package_dir):
    """Create package.py file"""
    package_py_path = os.path.join(package_dir, "package.py")

    # Convert package name to rez compatible format (use underscore instead of hyphen)
    rez_name = convert_name_to_rez_format(args.name)

    # Prepare template content
    package_content = f'''
# env variable is provided by Rez at runtime
# noqa: F821

name = "{rez_name}"
version = "{args.version}"

description = "Rez package for {args.name} Node.js package"

requires = [
    "nodejs-{args.node_version}+",
]

def commands():
'''

    # Add bin directory to PATH
    package_content += '''
    # Add bin directory to PATH
    env.PATH.append("{root}/bin")
'''

    # Add to NODE_PATH
    package_content += '''
    # Add to NODE_PATH
    if "NODE_PATH" not in env:
        env.NODE_PATH = "{root}/node_modules"
    else:
        env.NODE_PATH.append("{root}/node_modules")
'''

    # Write to file
    with open(package_py_path, "w", encoding="utf-8") as f:
        f.write(package_content)

    print(f"Created {package_py_path}")


def create_bin_files(args, bin_dir, package_name, bin_files):
    """Create binary files in bin directory

    Args:
        args: Command line arguments
        bin_dir: Directory to create binary files in
        package_name: Name of the package
        bin_files: List of binary file names
    """
    os.makedirs(bin_dir, exist_ok=True)

    for bin_file in bin_files:
        dst_path = os.path.join(bin_dir, bin_file)
        bin_name = os.path.splitext(bin_file)[0]

        if os.name == "nt":  # Windows
            # Skip PowerShell scripts
            if bin_file.endswith(".ps1"):
                continue

            # For .cmd files, create a portable batch file
            if bin_file.endswith(".cmd"):
                with open(dst_path, "w", encoding="utf-8") as f:
                    f.write("@echo off\n")
                    f.write(
                        f"node \"%~dp0\\..\\node_modules\\{package_name}\\bin\\{bin_name}\" %*\n"
                    )
            else:
                # For non-cmd files, create executable script
                if os.path.exists(dst_path):
                    os.remove(dst_path)
                with open(dst_path, "w", encoding="utf-8") as f:
                    f.write("#!/usr/bin/env node\n")
                    f.write(f"require(\"../node_modules/{package_name}/bin/{bin_name}\");\n")
                # Make the file executable
                os.chmod(dst_path, 0o755)
        else:  # Unix
            # On Unix, create executable script
            if os.path.exists(dst_path):
                os.remove(dst_path)

            with open(dst_path, "w", encoding="utf-8") as f:
                f.write("#!/usr/bin/env node\n")
                f.write(f"require(\"../node_modules/{package_name}/bin/{bin_name}\");\n")
            # Make the file executable
            os.chmod(dst_path, 0o755)


def get_npm_executable():
    """Find npm executable and check if it's available

    Returns:
        str or None: Path to npm executable or None if not found
    """
    npm = shutil.which("npm")

    # Check if npm is available
    try:
        if npm:
            subprocess.check_call(
                [npm, "--version"],
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL
            )
            return npm
        else:
            raise FileNotFoundError("npm not found")
    except (subprocess.SubprocessError, FileNotFoundError):
        return None


def install_from_npm(npm, args, install_path, is_test=False):
    """Install package from npm

    Args:
        npm: Path to npm executable
        args: Command line arguments
        install_path: Path to install package to
        is_test: Whether this is a test run

    Returns:
        bool: True if installation was successful
    """
    # Special handling for test environment
    if is_test:
        # For tests, just create the bin directory
        bin_dir = os.path.join(install_path, "bin")
        os.makedirs(bin_dir, exist_ok=True)

        # If there's a .bin directory in the install_path, use it
        local_bin_dir = os.path.join(install_path, "node_modules", ".bin")
        if os.path.exists(local_bin_dir):
            create_bin_files(args, bin_dir, args.name, os.listdir(local_bin_dir))

        print(f"Test mode: Skipped npm install for {args.name}@{args.version}")
        return True

    # Create temporary directory for npm installation
    temp_dir = os.path.join(os.path.dirname(install_path), "temp_npm")
    os.makedirs(temp_dir, exist_ok=True)

    try:
        # Create package.json in temporary directory
        package_json = {
            "name": "temp",
            "version": "1.0.0",
            "description": "Temporary package for npm2rez",
            "dependencies": {}
        }
        package_json["dependencies"][args.name] = args.version

        with open(os.path.join(temp_dir, "package.json"), "w") as f:
            json.dump(package_json, f, indent=2)

        # Install package in temporary directory
        subprocess.check_call([npm, "install"], cwd=temp_dir)

        # Create node_modules directory in install_path
        node_modules_dir = os.path.join(install_path, "node_modules")
        os.makedirs(node_modules_dir, exist_ok=True)

        # Copy only the target package from temp_dir to install_path
        temp_package_dir = os.path.join(temp_dir, "node_modules", args.name)
        if os.path.exists(temp_package_dir):
            target_package_dir = os.path.join(node_modules_dir, args.name)
            if os.path.exists(target_package_dir):
                shutil.rmtree(target_package_dir)
            shutil.copytree(temp_package_dir, target_package_dir)

            # Copy other dependencies if they exist
            temp_modules_dir = os.path.join(temp_dir, "node_modules")
            if os.path.exists(temp_modules_dir):
                for item in os.listdir(temp_modules_dir):
                    if (item != args.name and
                        item != ".package-lock.json" and
                        not item.startswith(".")):
                        src_path = os.path.join(temp_modules_dir, item)
                        dst_path = os.path.join(node_modules_dir, item)
                        if os.path.exists(dst_path):
                            shutil.rmtree(dst_path)
                        shutil.copytree(src_path, dst_path)

        # Create bin directory and binary files
        bin_dir = os.path.join(install_path, "bin")
        # Find binaries in node_modules/.bin
        temp_bin_dir = os.path.join(temp_dir, "node_modules", ".bin")
        if os.path.exists(temp_bin_dir):
            create_bin_files(args, bin_dir, args.name, os.listdir(temp_bin_dir))
        else:
            # For tests, check if there's a .bin directory in the install_path
            local_bin_dir = os.path.join(install_path, "node_modules", ".bin")
            if os.path.exists(local_bin_dir):
                create_bin_files(args, bin_dir, args.name, os.listdir(local_bin_dir))

        print(f"Installed {args.name}@{args.version} from npm")
        return True
    except Exception as e:
        print(f"Error installing from npm: {e}")
        return False
    finally:
        # Clean up temporary directory
        if os.path.exists(temp_dir):
            shutil.rmtree(temp_dir)


def install_from_github(npm, args, install_path):
    """Install package from GitHub

    Args:
        npm: Path to npm executable
        args: Command line arguments
        install_path: Path to install package to

    Returns:
        bool: True if installation was successful
    """
    repo_url = f"https://github.com/{args.repo}.git"
    temp_dir = os.path.join(os.path.dirname(install_path), "temp_repo")
    os.makedirs(temp_dir, exist_ok=True)

    try:
        # Clone to temporary directory
        subprocess.check_call([
            "git", "clone", "--depth", "1", "--branch", f"v{args.version}",
            repo_url, temp_dir
        ])

        # Install dependencies and build
        subprocess.check_call([npm, "install"], cwd=temp_dir)
        subprocess.check_call([npm, "run", "build"], cwd=temp_dir)

        # Create node_modules directory in install_path
        node_modules_dir = os.path.join(install_path, "node_modules")
        os.makedirs(node_modules_dir, exist_ok=True)

        # Copy only necessary files to installation directory
        for item in os.listdir(temp_dir):
            # Skip unnecessary files
            if item in [
                "package.json", "package-lock.json", ".git",
                ".github", ".npmignore", ".gitignore"
            ]:
                continue

            src_path = os.path.join(temp_dir, item)
            dst_path = os.path.join(install_path, item)

            if os.path.isdir(src_path):
                if item == "node_modules":
                    # For node_modules, only copy the package itself and its dependencies
                    if os.path.exists(src_path):
                        for module in os.listdir(src_path):
                            if not module.startswith("."):
                                module_src = os.path.join(src_path, module)
                                module_dst = os.path.join(node_modules_dir, module)
                                if os.path.exists(module_dst):
                                    shutil.rmtree(module_dst)
                                shutil.copytree(module_src, module_dst)
                else:
                    # Copy other directories
                    if os.path.exists(dst_path):
                        shutil.rmtree(dst_path)
                    shutil.copytree(src_path, dst_path)
            else:
                # Copy files
                shutil.copy2(src_path, dst_path)

        # Create bin directory and binary files
        bin_dir = os.path.join(install_path, "bin")
        # Find binaries in node_modules/.bin
        temp_bin_dir = os.path.join(temp_dir, "node_modules", ".bin")
        if os.path.exists(temp_bin_dir):
            create_bin_files(args, bin_dir, args.name, os.listdir(temp_bin_dir))
        else:
            # For tests, check if there's a .bin directory in the install_path
            local_bin_dir = os.path.join(install_path, "node_modules", ".bin")
            if os.path.exists(local_bin_dir):
                create_bin_files(args, bin_dir, args.name, os.listdir(local_bin_dir))

        print(f"Installed {args.name}@{args.version} from GitHub")
        return True
    except Exception as e:
        print(f"Error installing from GitHub: {e}")
        return False
    finally:
        # Clean up temporary directory
        if os.path.exists(temp_dir):
            shutil.rmtree(temp_dir)


def install_node_package(args, install_path):
    """Install Node.js package to specified directory

    Args:
        args: Command line arguments or object with the following attributes:
            name: Package name
            version: Package version
            source: Package source (npm or github)
            repo: GitHub repository (format: user/repo), required when source=github
            _is_test: Whether this is a test run (optional)
        install_path: Path to install package to

    Returns:
        bool: True if installation was successful
    """
    # Create installation directory
    os.makedirs(install_path, exist_ok=True)

    # Find npm executable
    npm = get_npm_executable()

    # Check if npm is available
    if not npm:
        # npm is not available
        print("Warning: npm command not found. Package files will be created but "
              "Node.js packages won't be installed.")
        print("Please install Node.js and npm to enable package installation.")
        # Create empty node_modules directory
        os.makedirs(os.path.join(install_path, "node_modules"), exist_ok=True)
        return False

    is_test = hasattr(args, "_is_test") and args._is_test

    if args.source == "npm":
        return install_from_npm(npm, args, install_path, is_test)
    else:
        return install_from_github(npm, args, install_path)


def extract_node_package(args, output_path):
    """Extract Node.js package to specified directory without creating package.py

    Args:
        args: Command line arguments or object with the following attributes:
            name: Package name
            version: Package version
            source: Package source (npm or github)
            repo: GitHub repository (format: user/repo), required when source=github
            _is_test: Whether this is a test run (optional)
        output_path: Path to extract package to

    Returns:
        bool: True if extraction was successful
    """
    # Create output directory
    os.makedirs(output_path, exist_ok=True)

    # Install Node.js package (same as install_node_package but without creating package.py)
    return install_node_package(args, output_path)


def convert_name_to_rez_format(name):
    """Convert package name to rez compatible format

    Args:
        name: Package name

    Returns:
        str: Rez compatible package name
    """
    return name.replace("-", "_").replace("@", "").replace("/", "_")
