"""Build Windows installer (MSI) using PyInstaller.

This script builds the WebDrop Bridge application for Windows using PyInstaller.
It creates both a standalone executable and optionally an MSI installer.

Requirements:
    - PyInstaller 6.0+
    - Python 3.10+
    - For MSI: WiX Toolset (optional, requires separate installation)

Usage:
    python build_windows.py [--msi] [--code-sign] [--env-file PATH]
    
    Options:
        --msi              Create MSI installer (requires WiX Toolset)
        --code-sign        Sign executable (requires certificate)
        --env-file PATH    Use custom .env file (default: project root .env)
                          If not provided, uses .env from project root
                          Build fails if .env doesn't exist
"""

import sys
import os
from typing import Optional

# Fix Unicode output on Windows BEFORE any other imports
if sys.platform == "win32":
    os.environ["PYTHONIOENCODING"] = "utf-8"
    import io
    # Reconfigure stdout/stderr for UTF-8 output
    sys.stdout = io.TextIOWrapper(
        sys.stdout.buffer, encoding="utf-8", errors="replace"
    )
    sys.stderr = io.TextIOWrapper(
        sys.stderr.buffer, encoding="utf-8", errors="replace"
    )

import subprocess
import shutil
import argparse
from pathlib import Path
from datetime import datetime

# Import shared version utilities
from sync_version import get_current_version, do_sync_version


class WindowsBuilder:
    """Build Windows installer using PyInstaller."""

    def __init__(self, env_file: Path | None = None):
        """Initialize builder paths.
        
        Args:
            env_file: Path to .env file to bundle. If None, uses project root .env.
                     If that doesn't exist, raises error.
        """
        self.project_root = Path(__file__).parent.parent.parent
        self.build_dir = self.project_root / "build"
        self.dist_dir = self.build_dir / "dist" / "windows"
        self.temp_dir = self.build_dir / "temp" / "windows"
        self.spec_file = self.build_dir / "webdrop_bridge.spec"
        self.version = get_current_version()
        
        # Validate and set env file
        if env_file is None:
            env_file = self.project_root / ".env"
        else:
            env_file = Path(env_file).resolve()
        
        if not env_file.exists():
            raise FileNotFoundError(
                f"Configuration file not found: {env_file}\n"
                f"Please provide a .env file using --env-file parameter\n"
                f"or ensure .env exists in project root"
            )
        
        self.env_file = env_file
        print(f"📋 Using configuration: {self.env_file}")

    def _get_version(self) -> str:
        """Get version from __init__.py.

        Note: This method is deprecated. Use get_current_version() from
        version_utils.py instead.
        """
        return get_current_version()

    def clean(self):
        """Clean previous builds."""
        print("🧹 Cleaning previous builds...")
        for path in [self.dist_dir, self.temp_dir]:
            if path.exists():
                shutil.rmtree(path)
                print(f"  Removed {path}")

    def build_executable(self) -> bool:
        """Build executable using PyInstaller."""
        print("\n🔨 Building Windows executable with PyInstaller...")

        self.dist_dir.mkdir(parents=True, exist_ok=True)
        self.temp_dir.mkdir(parents=True, exist_ok=True)

        # PyInstaller command using spec file
        # Pass env_file path as environment variable for spec to pick up
        cmd = [
            sys.executable,
            "-m",
            "PyInstaller",
            "--distpath",
            str(self.dist_dir),
            "--workpath",
            str(self.temp_dir),
            str(self.spec_file),
        ]

        print(f"  Command: {' '.join(cmd)}")
        
        # Set environment variable for spec file to use
        env = os.environ.copy()
        env["WEBDROP_ENV_FILE"] = str(self.env_file)
        
        result = subprocess.run(
            cmd, 
            cwd=str(self.project_root),
            text=True,
            env=env
        )

        if result.returncode != 0:
            print("❌ PyInstaller build failed")
            return False

        # Check if executable exists (now in WebDropBridge/WebDropBridge.exe due to COLLECT)
        exe_path = self.dist_dir / "WebDropBridge" / "WebDropBridge.exe"
        if not exe_path.exists():
            print(f"❌ Executable not found at {exe_path}")
            return False

        print("✅ Executable built successfully")
        print(f"📦 Output: {exe_path}")
        
        # Calculate total dist size
        total_size = sum(f.stat().st_size for f in self.dist_dir.glob("WebDropBridge/**/*") if f.is_file())
        if total_size > 0:
            print(f"   Total size: {total_size / 1024 / 1024:.1f} MB")

        # Set executable version information (required for MSI updates)
        self.set_exe_version(exe_path)

        # Generate SHA256 checksum
        self.generate_checksum(exe_path)

        return True

    def generate_checksum(self, file_path: Path) -> None:
        """Generate SHA256 checksum for file."""
        import hashlib

        print("\n📝 Generating SHA256 checksum...")
        sha256_hash = hashlib.sha256()

        with open(file_path, "rb") as f:
            for byte_block in iter(lambda: f.read(4096), b""):
                sha256_hash.update(byte_block)

        checksum = sha256_hash.hexdigest()
        checksum_file = file_path.parent / f"{file_path.name}.sha256"

        checksum_file.write_text(checksum)
        print(f"✅ Checksum generated")
        print(f"   File: {checksum_file}")
        print(f"   Hash: {checksum}")

    def set_exe_version(self, exe_path: Path) -> None:
        """Set executable file version for Windows.
        
        This is important for MSI updates: Windows Installer compares file versions
        to determine if files should be updated during a major upgrade.
        
        Args:
            exe_path: Path to the executable file
        """
        print("\n🏷️  Setting executable version information...")
        
        try:
            import pefile
        except ImportError:
            print("⚠️  pefile not installed - skipping EXE version update")
            print("   Note: Install with: pip install pefile")
            print("   EXE version info will be blank (MSI updates may not work correctly)")
            return
        
        try:
            pe = pefile.PE(str(exe_path))
            
            # Parse version into 4-part format (Major, Minor, Build, Revision)
            version_parts = self.version.split(".")
            while len(version_parts) < 4:
                version_parts.append("0")
            
            file_version = tuple(int(v) for v in version_parts[:4])
            
            # Set version resource if it exists
            if hasattr(pe, "VS_FIXEDFILEINFO"):
                pe.VS_FIXEDFILEINFO[0].FileVersionMS = (file_version[0] << 16) | file_version[1]
                pe.VS_FIXEDFILEINFO[0].FileVersionLS = (file_version[2] << 16) | file_version[3]
                pe.VS_FIXEDFILEINFO[0].ProductVersionMS = (file_version[0] << 16) | file_version[1]
                pe.VS_FIXEDFILEINFO[0].ProductVersionLS = (file_version[2] << 16) | file_version[3]
                
                # Write modified PE back to file
                pe.write(filename=str(exe_path))
                print(f"✅ Version set to {self.version}")
            else:
                print("⚠️  No version resource found in EXE")
                
        except Exception as e:
            print(f"⚠️  Could not set EXE version: {e}")
            print("   MSI updates may not work correctly without file version info")

    def create_msi(self) -> bool:
        """Create MSI installer using WiX Toolset.

        This requires WiX Toolset to be installed:
        https://wixtoolset.org/releases/
        """
        print("\n📦 Creating MSI installer with WiX...")

        # Check if WiX is installed (try PATH first, then default location)
        heat_exe = shutil.which("heat.exe")
        candle_exe = shutil.which("candle.exe")
        light_exe = shutil.which("light.exe")

        # Fallback to default WiX installation location
        if not candle_exe:
            default_wix = Path("C:\\Program Files (x86)\\WiX Toolset v3.14\\bin")
            if default_wix.exists():
                heat_exe = str(default_wix / "heat.exe")
                candle_exe = str(default_wix / "candle.exe")
                light_exe = str(default_wix / "light.exe")

        if not all([heat_exe, candle_exe, light_exe]):
            print("⚠️  WiX Toolset not found in PATH or default location")
            print("  Install from: https://wixtoolset.org/releases/")
            print("  Or use: choco install wixtoolset")
            return False

        # Create base WiX source file
        if not self._create_wix_source():
            return False

        # Harvest application files using Heat
        print(f"  Harvesting application files...")
        dist_folder = self.dist_dir / "WebDropBridge"
        if not dist_folder.exists():
            print(f"❌ Distribution folder not found: {dist_folder}")
            return False
        
        harvest_file = self.build_dir / "WebDropBridge_Files.wxs"
        
        # Use Heat to harvest all files
        heat_cmd = [
            str(heat_exe),
            "dir",
            str(dist_folder),
            "-cg", "AppFiles",
            "-dr", "INSTALLFOLDER",
            "-sfrag",
            "-srd",
            "-gg",
            "-o", str(harvest_file),
        ]
        
        result = subprocess.run(heat_cmd, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        if result.returncode != 0:
            print("⚠️  Heat harvest warnings (may be non-critical)")
            if result.stderr:
                print(result.stderr[:200])  # Show first 200 chars of errors
        else:
            print(f"  ✓ Harvested files")
        
        # Post-process harvested file to mark components as 64-bit
        if harvest_file.exists():
            content = harvest_file.read_text()
            # Add Win64="yes" to all Component tags
            content = content.replace(
                '<Component ',
                '<Component Win64="yes" '
            )
            harvest_file.write_text(content)
            print(f"  ✓ Marked components as 64-bit")

        # Compile both WiX files
        wix_obj = self.build_dir / "WebDropBridge.wixobj"
        wix_files_obj = self.build_dir / "WebDropBridge_Files.wixobj"
        msi_output = self.dist_dir / f"WebDropBridge-{self.version}-Setup.msi"

        # Run candle compiler - make sure to use correct source directory
        candle_cmd = [
            str(candle_exe),
            "-ext", "WixUIExtension",
            f"-dDistDir={self.dist_dir}",
            f"-dSourceDir={self.dist_dir}\\WebDropBridge",  # Set SourceDir for Heat-generated files
            f"-dResourcesDir={self.project_root}\\resources",  # Set ResourcesDir for branding assets
            "-o", str(self.build_dir) + "\\",
            str(self.build_dir / "WebDropBridge.wxs"),
        ]
        
        if harvest_file.exists():
            candle_cmd.append(str(harvest_file))

        print(f"  Compiling WiX source...")
        result = subprocess.run(candle_cmd, text=True, cwd=str(self.build_dir))
        if result.returncode != 0:
            print("❌ WiX compilation failed")
            return False

        # Link MSI - include both obj files if harvest was successful
        light_cmd = [
            str(light_exe),
            "-ext", "WixUIExtension",
            "-b", str(self.dist_dir / "WebDropBridge"),  # Base path for source files
            "-o", str(msi_output),
            str(wix_obj),
        ]
        
        if wix_files_obj.exists():
            light_cmd.append(str(wix_files_obj))

        print(f"  Linking MSI installer...")
        result = subprocess.run(light_cmd, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        if result.returncode != 0:
            print("❌ MSI linking failed")
            if result.stdout:
                print(f"  Output: {result.stdout[:500]}")
            if result.stderr:
                print(f"  Error: {result.stderr[:500]}")
            return False

        if not msi_output.exists():
            print(f"❌ MSI not found at {msi_output}")
            return False

        print("✅ MSI installer created successfully")
        print(f"📦 Output: {msi_output}")
        print(f"   Size: {msi_output.stat().st_size / 1024 / 1024:.1f} MB")

        return True

    def _create_wix_source(self) -> bool:
        """Create WiX source file for MSI generation.
        
        Creates per-machine installation (Program Files).
        Installation requires admin rights, but the app does not.
        """
        wix_content = f'''<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
     xmlns:ui="http://schemas.microsoft.com/wix/2010/ui">
    <Product Id="*" Name="WebDrop Bridge" Language="1033" Version="{self.version}"
        Manufacturer="HIM-Tools"
        UpgradeCode="12345678-1234-1234-1234-123456789012">

        <Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" Platform="x64" />
        <Media Id="1" Cabinet="WebDropBridge.cab" EmbedCab="yes" />
        
        <!-- Required property for WixUI_InstallDir dialog set -->
        <Property Id="WIXUI_INSTALLDIR" Value="INSTALLFOLDER" />
        
        <!-- Application Icon -->
        <Icon Id="AppIcon.ico" SourceFile="$(var.ResourcesDir)\\icons\\app.ico" />
        
        <!-- Custom branding for InstallDir dialog set -->
        <WixVariable Id="WixUIDialogBmp" Value="$(var.ResourcesDir)\\icons\\background.bmp" />
        <WixVariable Id="WixUIBannerBmp" Value="$(var.ResourcesDir)\\icons\\banner.bmp" />
        <WixVariable Id="WixUILicenseRtf" Value="$(var.ResourcesDir)\\license.rtf" />

        <!-- Installation UI dialogs -->
        <UIRef Id="WixUI_InstallDir" />
        <UIRef Id="WixUI_ErrorProgressText" />

        <Feature Id="ProductFeature" Title="WebDrop Bridge" Level="1">
            <ComponentGroupRef Id="AppFiles" />
            <ComponentRef Id="ProgramMenuShortcut" />
            <ComponentRef Id="DesktopShortcut" />
        </Feature>

        <Directory Id="TARGETDIR" Name="SourceDir">
            <Directory Id="ProgramFiles64Folder">
                <Directory Id="INSTALLFOLDER" Name="WebDrop Bridge" />
            </Directory>
            <Directory Id="ProgramMenuFolder">
                <Directory Id="ApplicationProgramsFolder" Name="WebDrop Bridge"/>
            </Directory>
            <Directory Id="DesktopFolder" />
        </Directory>

        <DirectoryRef Id="ApplicationProgramsFolder">
            <Component Id="ProgramMenuShortcut" Guid="*">
                <Shortcut Id="ApplicationStartMenuShortcut"
                    Name="WebDrop Bridge"
                    Description="Web Drag-and-Drop Bridge"
                    Target="[INSTALLFOLDER]WebDropBridge.exe"
                    Icon="AppIcon.ico"
                    IconIndex="0"
                    WorkingDirectory="INSTALLFOLDER" />
                <RemoveFolder Id="ApplicationProgramsFolderRemove"
                    On="uninstall" />
                <RegistryValue Root="HKCU"
                    Key="Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\WebDropBridge"
                    Name="installed"
                    Type="integer"
                    Value="1"
                    KeyPath="yes" />
            </Component>
        </DirectoryRef>

        <DirectoryRef Id="DesktopFolder">
            <Component Id="DesktopShortcut" Guid="*">
                <Shortcut Id="DesktopApplicationShortcut"
                    Name="WebDrop Bridge"
                    Description="Web Drag-and-Drop Bridge"
                    Target="[INSTALLFOLDER]WebDropBridge.exe"
                    Icon="AppIcon.ico"
                    IconIndex="0"
                    WorkingDirectory="INSTALLFOLDER" />
                <RegistryValue Root="HKCU"
                    Key="Software\\WebDropBridge"
                    Name="DesktopShortcut"
                    Type="integer"
                    Value="1"
                    KeyPath="yes" />
            </Component>
        </DirectoryRef>
    </Product>
</Wix>
'''

        wix_file = self.build_dir / "WebDropBridge.wxs"
        wix_file.write_text(wix_content)
        print(f"  Created WiX source: {wix_file}")
        return True

    def _generate_file_elements(self, folder: Path, parent_dir_ref: str, parent_rel_path: str, indent: int = 8, file_counter: Optional[dict] = None) -> str:
        """Generate WiX File elements for all files in a folder.
        
        Args:
            folder: Root folder to scan
            parent_dir_ref: Parent WiX DirectoryRef ID
            parent_rel_path: Relative path for component structure
            indent: Indentation level
            file_counter: Dictionary to track file IDs for uniqueness
            
        Returns:
            WiX XML string with all File elements
        """
        if file_counter is None:
            file_counter = {}
            
        elements = []
        indent_str = " " * indent
        
        try:
            # Get all files in current folder
            for item in sorted(folder.iterdir()):
                if item.is_file():
                    # Create unique File element ID using hash of full path
                    import hashlib
                    path_hash = hashlib.md5(str(item).encode()).hexdigest()[:8]
                    file_id = f"File_{path_hash}"
                    file_path = str(item)
                    elements.append(f'{indent_str}<File Id="{file_id}" Source="{file_path}" />')
                elif item.is_dir() and item.name != "__pycache__":
                    # Recursively add files from subdirectories
                    sub_elements = self._generate_file_elements(
                        item, parent_dir_ref, 
                        f"{parent_rel_path}/{item.name}",
                        indent,
                        file_counter
                    )
                    if sub_elements:
                        elements.append(sub_elements)
        except PermissionError:
            print(f"  ⚠️  Permission denied accessing {folder}")
        
        return "\n".join(elements)

    def _sanitize_id(self, filename: str) -> str:
        """Sanitize filename to be a valid WiX identifier.
        
        Args:
            filename: Filename to sanitize
            
        Returns:
            Sanitized identifier
        """
        # Remove extension and invalid characters
        safe_name = filename.rsplit(".", 1)[0] if "." in filename else filename
        # Replace invalid characters with underscores
        safe_name = "".join(c if c.isalnum() or c == "_" else "_" for c in safe_name)
        # Ensure it starts with a letter or underscore
        if safe_name and not (safe_name[0].isalpha() or safe_name[0] == "_"):
            safe_name = f"_{safe_name}"
        # Limit length to avoid WiX ID limits
        return safe_name[:50] if len(safe_name) > 50 else safe_name

    def sign_executable(self, cert_path: str, password: str) -> bool:
        """Sign executable with certificate (optional).

        Args:
            cert_path: Path to code signing certificate
            password: Certificate password

        Returns:
            True if signing successful
        """
        print("\n🔐 Signing executable...")

        signtool = shutil.which("signtool.exe")
        if not signtool:
            print("⚠️  signtool.exe not found (part of Windows SDK)")
            print("  Skipping code signing")
            return True

        exe_path = self.dist_dir / "WebDropBridge.exe"
        cmd = [
            signtool,
            "sign",
            "/f",
            cert_path,
            "/p",
            password,
            "/t",
            "http://timestamp.comodoca.com/authenticode",
            str(exe_path),
        ]

        result = subprocess.run(
            cmd,
            text=True
        )
        if result.returncode != 0:
            print("❌ Code signing failed")
            return False

        print("✅ Executable signed successfully")
        return True

    def build(self, create_msi: bool = False, sign: bool = False) -> bool:
        """Run complete build process.

        Args:
            create_msi: Whether to create MSI installer
            sign: Whether to sign executable (requires certificate)

        Returns:
            True if build successful
        """
        start_time = datetime.now()
        print("=" * 60)
        print("🚀 WebDrop Bridge Windows Build")
        print("=" * 60)

        self.clean()

        if not self.build_executable():
            return False

        if create_msi:
            if not self.create_msi():
                print("⚠️  MSI creation failed, but executable is available")

        if sign:
            # Would need certificate path from environment
            cert_path = os.getenv("CODE_SIGN_CERT")
            if cert_path:
                self.sign_executable(cert_path, os.getenv("CODE_SIGN_PASSWORD", ""))

        elapsed = (datetime.now() - start_time).total_seconds()
        print("\n" + "=" * 60)
        print(f"✅ Build completed in {elapsed:.1f}s")
        print("=" * 60)
        return True


def main() -> int:
    """Build Windows MSI installer."""
    parser = argparse.ArgumentParser(
        description="Build WebDrop Bridge Windows installer"
    )
    parser.add_argument(
        "--msi",
        action="store_true",
        help="Create MSI installer (requires WiX Toolset)",
    )
    parser.add_argument(
        "--code-sign",
        action="store_true",
        help="Sign executable (requires certificate in CODE_SIGN_CERT env var)",
    )
    parser.add_argument(
        "--env-file",
        type=str,
        default=None,
        help="Path to .env file to bundle (default: project root .env)",
    )
    
    args = parser.parse_args()
    
    print("🔄 Syncing version...")
    do_sync_version()

    try:
        builder = WindowsBuilder(env_file=args.env_file)
    except FileNotFoundError as e:
        print(f"❌ Build failed: {e}")
        return 1

    success = builder.build(create_msi=args.msi, sign=args.code_sign)

    return 0 if success else 1


if __name__ == "__main__":
    sys.exit(main())
