"""
Build a Basilisk project into a standalone executable using PyInstaller.

Run with: basilisk file.py or basilisk-engine file.py

Also supports: basilisk path/to/script.py, basilisk script.spec, and options
like --work-dir, --source-dir, --no-copy, etc.
"""

import re
import shutil
import subprocess
import sys
from pathlib import Path


def get_pyinstaller_output_dir(spec_file_or_script):
    """
    Determine the output directory name from a spec file or Python script.
    Returns the name that PyInstaller will use for the dist/ subdirectory.
    """
    path = Path(spec_file_or_script)

    if path.suffix == '.spec':
        with open(path, 'r') as f:
            content = f.read()
            match = re.search(r"name=['\"]([^'\"]+)['\"]", content)
            if match:
                return match.group(1)
            return path.stem
    else:
        return path.stem


def run_pyinstaller(spec_file_or_script, work_dir=None):
    """Run PyInstaller on the given file."""
    if work_dir is None:
        work_dir = Path(spec_file_or_script).parent

    work_dir = Path(work_dir)
    spec_file_or_script = Path(spec_file_or_script)

    if not spec_file_or_script.is_absolute():
        spec_file_or_script = work_dir / spec_file_or_script
    else:
        try:
            spec_file_or_script = spec_file_or_script.relative_to(work_dir)
        except ValueError:
            pass

    print(f"Running PyInstaller on: {spec_file_or_script}")
    print(f"Working directory: {work_dir}")

    result = subprocess.run(
        [sys.executable, '-m', 'PyInstaller', str(spec_file_or_script)],
        cwd=work_dir,
        check=True
    )

    return result


def copy_basilisk_resources(internal_dir):
    """
    Copy basilisk package resources (shaders, models, textures) to _internal/basilisk/.
    These are needed at runtime for the engine to find default resources.
    """
    try:
        import basilisk
        basilisk_package_dir = Path(basilisk.__file__).parent
    except ImportError:
        print("Warning: Could not import basilisk package. Skipping resource copy.")
        print("  Make sure basilisk is installed: pip install -e .")
        return
    
    # The resources should be in the same directory as the .so/.pyd file
    # (shaders/, models/, textures/ directories)
    basilisk_resources = ['shaders', 'models', 'textures']
    basilisk_dest = internal_dir / 'basilisk'
    basilisk_dest.mkdir(parents=True, exist_ok=True)
    
    print(f"Copying basilisk resources from {basilisk_package_dir} to {basilisk_dest}")
    
    copied_count = 0
    for resource_dir in basilisk_resources:
        source_path = basilisk_package_dir / resource_dir
        dest_path = basilisk_dest / resource_dir
        
        if source_path.exists() and source_path.is_dir():
            if dest_path.exists():
                shutil.rmtree(dest_path)
            shutil.copytree(source_path, dest_path, dirs_exist_ok=True)
            print(f"  Copied: {resource_dir}/")
            copied_count += 1
        else:
            print(f"  Warning: {resource_dir}/ not found in {basilisk_package_dir}")
    
    if copied_count == 0:
        print("  Warning: No basilisk resources found. The application may not work correctly.")
    else:
        print(f"  Successfully copied {copied_count} resource directories")


def copy_top_level_files(source_dir, dest_dir, exclude_patterns=None):
    """
    Copy all top-level files and folders from source_dir to dest_dir,
    excluding PyInstaller-generated files and patterns in exclude_patterns.
    """
    if exclude_patterns is None:
        exclude_patterns = []

    default_excludes = {
        'build',
        'dist',
        '__pycache__',
        '*.pyc',
        '*.pyo',
        '*.spec',
        '.pytest_cache',
        '.mypy_cache',
        '*.egg-info',
    }

    all_excludes = set(default_excludes) | set(exclude_patterns)

    source_dir = Path(source_dir)
    dest_dir = Path(dest_dir)

    if not source_dir.exists():
        print(f"Warning: Source directory {source_dir} does not exist")
        return

    dest_dir.mkdir(parents=True, exist_ok=True)

    print(f"Copying files from {source_dir} to {dest_dir}")

    copied_count = 0
    skipped_count = 0

    for item in source_dir.iterdir():
        should_exclude = False
        for pattern in all_excludes:
            if pattern.endswith('*'):
                if item.name.startswith(pattern[:-1]) or item.suffix in ['.pyc', '.pyo']:
                    should_exclude = True
                    break
            elif item.name == pattern or item.name.startswith(pattern):
                should_exclude = True
                break

        if should_exclude:
            print(f"  Skipping: {item.name}")
            skipped_count += 1
            continue

        dest_item = dest_dir / item.name

        try:
            if item.is_file():
                shutil.copy2(item, dest_item)
                print(f"  Copied file: {item.name}")
                copied_count += 1
            elif item.is_dir():
                if dest_item.exists():
                    shutil.rmtree(dest_item)
                shutil.copytree(item, dest_item, dirs_exist_ok=True)
                print(f"  Copied directory: {item.name}/")
                copied_count += 1
        except Exception as e:
            print(f"  Error copying {item.name}: {e}")

    print(f"\nCopy complete: {copied_count} items copied, {skipped_count} items skipped")


def main(argv=None):
    """Entry point for the basilisk / basilisk-engine CLI."""
    parser = _create_parser()
    args = parser.parse_args(argv)

    # Determine working directory
    file_path = Path(args.file)
    if not file_path.is_absolute():
        file_path = Path.cwd() / file_path

    if args.work_dir:
        work_dir = Path(args.work_dir)
    else:
        work_dir = file_path.parent

    if args.source_dir:
        source_dir = Path(args.source_dir)
    else:
        source_dir = work_dir

    # Step 1: Run PyInstaller
    print("=" * 60)
    print("Step 1: Running PyInstaller")
    print("=" * 60)
    try:
        run_pyinstaller(file_path, work_dir)
    except subprocess.CalledProcessError as e:
        print(f"Error: PyInstaller failed with exit code {e.returncode}")
        sys.exit(1)

    # Step 2: Find the _internal directory
    output_name = get_pyinstaller_output_dir(file_path)
    dist_dir = work_dir / 'dist' / output_name
    internal_dir = dist_dir / '_internal'

    if not internal_dir.exists():
        print(f"\nWarning: _internal directory not found at {internal_dir}")
        print("PyInstaller may have used a different output structure.")
        print("Please check the dist/ directory manually.")
        return

    # Step 3: Copy basilisk package resources to _internal/basilisk/
    print("\n" + "=" * 60)
    print("Step 2: Copying basilisk package resources")
    print("=" * 60)
    copy_basilisk_resources(internal_dir)

    # Step 4: Copy files to _internal (if not skipped)
    if not args.no_copy:
        print("\n" + "=" * 60)
        print("Step 3: Copying top-level files to _internal/")
        print("=" * 60)
        copy_top_level_files(source_dir, internal_dir, args.exclude)

    # Step 5: Move dist/<name> to top level, rename to 'build', and clean up
    print("\n" + "=" * 60)
    print("Step 4: Reorganizing output structure")
    print("=" * 60)

    final_build_dir = work_dir / 'build'

    pyinstaller_build_dir = work_dir / 'build' / output_name
    if pyinstaller_build_dir.exists():
        print(f"Removing PyInstaller build directory: {pyinstaller_build_dir}")
        shutil.rmtree(pyinstaller_build_dir)

    pyinstaller_build_parent = work_dir / 'build'
    if pyinstaller_build_parent.exists():
        try:
            if not any(pyinstaller_build_parent.iterdir()):
                pyinstaller_build_parent.rmdir()
                print(f"Removed empty PyInstaller build parent: {pyinstaller_build_parent}")
        except Exception:
            pass

    if final_build_dir.exists():
        print(f"Removing existing build directory: {final_build_dir}")
        shutil.rmtree(final_build_dir)

    print(f"Moving {dist_dir} to {final_build_dir}")
    shutil.move(str(dist_dir), str(final_build_dir))

    # Clean up
    print("Cleaning up PyInstaller-generated files...")

    dist_parent = work_dir / 'dist'
    if dist_parent.exists():
        try:
            remaining_items = list(dist_parent.iterdir())
            if remaining_items:
                print(f"  Note: dist/ directory contains other builds, keeping it")
            else:
                dist_parent.rmdir()
                print(f"  Removed: {dist_parent}")
        except Exception as e:
            print(f"  Warning: Could not remove {dist_parent}: {e}")

    spec_file_path = file_path if file_path.suffix == '.spec' else work_dir / f"{output_name}.spec"
    if spec_file_path.exists() and spec_file_path.suffix == '.spec':
        spec_file_path.unlink()
        print(f"  Removed: {spec_file_path}")

    print("\n" + "=" * 60)
    print("Build complete!")
    print(f"Final output directory: {final_build_dir}")
    print(f"Executable: {final_build_dir / output_name}")
    print("=" * 60)


def _create_parser():
    import argparse
    parser = argparse.ArgumentParser(
        description='Build a Basilisk project into a standalone executable (PyInstaller)'
    )
    parser.add_argument(
        'file',
        nargs='?',
        default='main.py',
        help='Python script or .spec file to build (default: main.py)'
    )
    parser.add_argument(
        '--work-dir',
        type=str,
        default=None,
        help='Working directory (default: directory containing the file)'
    )
    parser.add_argument(
        '--source-dir',
        type=str,
        default=None,
        help='Source directory to copy files from (default: work-dir)'
    )
    parser.add_argument(
        '--exclude',
        action='append',
        default=[],
        help='Additional patterns to exclude (can be used multiple times)'
    )
    parser.add_argument(
        '--no-copy',
        action='store_true',
        help='Skip copying files, only run PyInstaller'
    )
    return parser
