#!/usr/bin/env python3
"""
Ubuntu ISO Builder for Autoinstall
Follows the Ubuntu autoinstall guide exactly:
1. Download Ubuntu ISO
2. Extract with 7zip equivalent
3. Modify GRUB configuration
4. Add server/ directory with autoinstall config
5. Rebuild ISO with xorriso equivalent
"""

import os
import logging
import subprocess
import tempfile
import shutil
import urllib.request
from pathlib import Path
from typing import Optional

logger = logging.getLogger(__name__)

# Ubuntu ISO URLs with fallbacks
UBUNTU_MIRRORS = [
    "https://releases.ubuntu.com",  # Official Ubuntu releases (primary)
    "http://archive.ubuntu.com/ubuntu-releases",  # Official archive
    "http://mirror.csclub.uwaterloo.ca/ubuntu-releases",  # University of Waterloo
    "http://mirror.math.princeton.edu/pub/ubuntu-releases",  # Princeton mirror
]
UBUNTU_24_04_ISO = "24.04/ubuntu-24.04.3-live-server-amd64.iso"
UBUNTU_22_04_ISO = "22.04/ubuntu-22.04.3-live-server-amd64.iso"


def get_latest_ubuntu_server_iso(version: str) -> Optional[str]:
    """Dynamically find the latest point release for a given Ubuntu version."""
    try:
        import re

        for mirror in UBUNTU_MIRRORS:
            try:
                url = f"{mirror}/{version}/"
                response = urllib.request.urlopen(url, timeout=10)
                content = response.read().decode("utf-8")

                # Find all server ISO files for this version
                pattern = rf"ubuntu-{
                    re.escape(version)}\.[0-9]+-live-server-amd64\.iso"
                matches = re.findall(pattern, content)

                if matches:
                    # Sort by version and return the latest
                    matches.sort(key=lambda x: [int(n) for n in re.findall(r"\d+", x)])
                    latest_iso = matches[-1]
                    return f"{version}/{latest_iso}"
            except Exception as e:
                logger.debug(f"Failed to check {mirror}/{version}/: {e}")
                continue

        logger.warning(f"Could not dynamically detect latest ISO for Ubuntu {version}")
        return None

    except Exception as e:
        logger.error(f"Error in dynamic ISO detection: {e}")
        return None


class UbuntuISOBuilder:
    """Builds modified Ubuntu ISO with autoinstall configuration."""

    def __init__(self, vm_name: str, work_dir: Optional[str] = None):
        self.vm_name = vm_name
        self.work_dir = (
            Path(work_dir)
            if work_dir
            else Path(tempfile.mkdtemp(prefix="ubuntu-autoinstall-"))
        )
        self.source_files_dir = self.work_dir / "source-files"
        self.boot_dir = self.work_dir / "BOOT"
        self.server_dir = self.source_files_dir / "server"
        self.grub_cfg_path = self.source_files_dir / "boot" / "grub" / "grub.cfg"

        # Ensure directories exist
        self.work_dir.mkdir(exist_ok=True, parents=True)
        self.source_files_dir.mkdir(exist_ok=True, parents=True)

    def check_tools(self) -> bool:
        """Check if required tools are available."""

        # Check for 7zip equivalent (p7zip on macOS/Linux)
        if not shutil.which("7z") and not shutil.which("7za"):
            logger.error(
                "7zip not found. Install with: brew install p7zip (macOS) or apt install p7zip-full (Ubuntu)"
            )
            return False

        # Check for xorriso equivalent
        if (
            not shutil.which("xorriso")
            and not shutil.which("mkisofs")
            and not shutil.which("hdiutil")
        ):
            logger.error(
                "No ISO creation tool found. Install xorriso, mkisofs, or use macOS hdiutil"
            )
            return False

        return True

    def download_ubuntu_iso(self, version: str = "24.04") -> Path:
        """Download Ubuntu ISO if not already present, trying multiple mirrors."""
        iso_filename = f"ubuntu-{version}-live-server-amd64.iso"
        iso_path = self.work_dir / iso_filename

        if iso_path.exists():
            logger.info(f"Ubuntu ISO already exists: {iso_path}")
            return iso_path

        if version == "24.04":
            iso_subpath = UBUNTU_24_04_ISO
        elif version == "22.04":
            iso_subpath = UBUNTU_22_04_ISO
        else:
            raise ValueError(f"Unsupported Ubuntu version: {version}")

        # Try each mirror until one works
        last_error = None
        for mirror in UBUNTU_MIRRORS:
            iso_url = f"{mirror}/{iso_subpath}"
            logger.info(f"Trying to download Ubuntu {version} ISO from {iso_url}")

            try:
                # Try downloading from this mirror
                urllib.request.urlretrieve(iso_url, iso_path)
                logger.info(
                    f"✅ Ubuntu ISO downloaded successfully from {mirror}: {iso_path}"
                )
                return iso_path
            except Exception as e:
                last_error = e
                logger.warning(f"Failed to download from {mirror}: {e}")
                # Remove partial download if it exists
                if iso_path.exists():
                    iso_path.unlink()
                continue

        # If we get here, all mirrors failed
        logger.error(
            f"Failed to download Ubuntu ISO from all mirrors. Last error: {last_error}"
        )
        raise last_error

    def extract_iso(self, iso_path: Path) -> bool:
        """Extract Ubuntu ISO following the guide."""
        logger.info(f"Extracting ISO: {iso_path}")

        # Use 7z to extract ISO
        seven_zip_cmd = "7z" if shutil.which("7z") else "7za"

        try:
            # Extract ISO: 7z -y x ubuntu.iso -osource-files
            subprocess.run(
                [
                    seven_zip_cmd,
                    "-y",
                    "x",
                    str(iso_path),
                    f"-o{self.source_files_dir}",
                ],
                capture_output=True,
                text=True,
                check=True,
            )

            logger.info("ISO extracted successfully")

            # Move [BOOT] directory as per guide: mv '[BOOT]' ../BOOT
            boot_source = self.source_files_dir / "[BOOT]"
            if boot_source.exists():
                shutil.move(str(boot_source), str(self.boot_dir))
                logger.info(f"Moved [BOOT] directory to {self.boot_dir}")
            else:
                logger.warning("[BOOT] directory not found in extracted files")

            return True

        except subprocess.CalledProcessError as e:
            logger.error(f"Failed to extract ISO: {e.stderr}")
            return False
        except Exception as e:
            logger.error(f"Error extracting ISO: {e}")
            return False

    def modify_grub_config(self) -> bool:
        """Modify GRUB configuration to add autoinstall menu entry."""
        logger.info("Modifying GRUB configuration...")

        if not self.grub_cfg_path.exists():
            logger.error(f"GRUB config not found: {self.grub_cfg_path}")
            return False

        try:
            # Read existing GRUB config
            with open(self.grub_cfg_path, "r", encoding="utf-8") as f:
                grub_content = f.read()

            # Autoinstall menu entry as per guide
            autoinstall_entry = """menuentry "Autoinstall Ubuntu Server" {
    set gfxpayload=keep
    linux   /casper/vmlinuz quiet autoinstall ds=nocloud\\;s=/cdrom/server/  ---
    initrd  /casper/initrd
}

"""

            # Insert autoinstall entry at the beginning of menu entries
            # Find the first menuentry and insert before it
            import re

            first_menu_match = re.search(r'(menuentry\s+["\'])', grub_content)
            if first_menu_match:
                insert_pos = first_menu_match.start()
                modified_content = (
                    grub_content[:insert_pos]
                    + autoinstall_entry
                    + grub_content[insert_pos:]
                )
            else:
                # Fallback: append at the end
                modified_content = grub_content + "\n" + autoinstall_entry

            # Write modified GRUB config
            with open(self.grub_cfg_path, "w", encoding="utf-8") as f:
                f.write(modified_content)

            logger.info("GRUB configuration modified successfully")
            return True

        except Exception as e:
            logger.error(f"Failed to modify GRUB config: {e}")
            return False

    def create_autoinstall_config(self, user_data: str) -> bool:
        """Create autoinstall configuration in server/ directory."""
        logger.info("Creating autoinstall configuration...")

        try:
            # Create server directory
            self.server_dir.mkdir(exist_ok=True, parents=True)

            # Create empty meta-data file (as per guide)
            meta_data_path = self.server_dir / "meta-data"
            meta_data_path.touch()
            logger.info(f"Created empty meta-data: {meta_data_path}")

            # Create user-data file with autoinstall configuration
            user_data_path = self.server_dir / "user-data"
            with open(user_data_path, "w", encoding="utf-8") as f:
                f.write(user_data)
            logger.info(f"Created user-data: {user_data_path}")

            return True

        except Exception as e:
            logger.error(f"Failed to create autoinstall config: {e}")
            return False

    def rebuild_iso(self, output_path: Path) -> bool:
        """Rebuild ISO with autoinstall configuration using xorriso."""
        logger.info(f"Rebuilding ISO: {output_path}")

        try:
            # Change to source-files directory for xorriso command
            original_cwd = os.getcwd()
            os.chdir(self.source_files_dir)

            # Remove existing output file
            if output_path.exists():
                output_path.unlink()

            # Try different ISO creation methods in order of preference
            success = False

            # Method 1: xorriso (most complete)
            if shutil.which("xorriso") and not success:
                try:
                    logger.info("Trying xorriso method...")
                    cmd = [
                        "xorriso",
                        "-as",
                        "mkisofs",
                        "-r",
                        "-V",
                        f"Ubuntu 24.04 LTS AUTO (EFIBIOS)",
                        "-o",
                        str(output_path),
                        "--grub2-mbr",
                        f"..{os.sep}BOOT{os.sep}1-Boot-NoEmul.img",
                        "-partition_offset",
                        "16",
                        "--mbr-force-bootable",
                        "-append_partition",
                        "2",
                        "28732ac11ff8d211ba4b00a0c93ec93b",
                        f"..{os.sep}BOOT{os.sep}2-Boot-NoEmul.img",
                        "-appended_part_as_gpt",
                        "-iso_mbr_part_type",
                        "a2a0d0ebe5b9334487c068b6b72699c7",
                        "-c",
                        "/boot.catalog",
                        "-b",
                        "/boot/grub/i386-pc/eltorito.img",
                        "-no-emul-boot",
                        "-boot-load-size",
                        "4",
                        "-boot-info-table",
                        "--grub2-boot-info",
                        "-eltorito-alt-boot",
                        "-e",
                        "--interval:appended_partition_2:::",
                        "-no-emul-boot",
                        ".",
                    ]
                    subprocess.run(cmd, capture_output=True, text=True, check=True)
                    success = True
                    logger.info("✅ ISO created with xorriso")
                except subprocess.CalledProcessError as e:
                    logger.warning(f"xorriso failed: {e.stderr}")
                    if output_path.exists():
                        output_path.unlink()

            # Method 2: mkisofs with joliet-long
            if shutil.which("mkisofs") and not success:
                try:
                    logger.info("Trying mkisofs with joliet-long...")
                    cmd = [
                        "mkisofs",
                        "-r",
                        "-V",
                        f"Ubuntu 24.04 LTS AUTO",
                        "-cache-inodes",
                        "-J",
                        "-joliet-long",
                        "-l",
                        "-b",
                        "boot/grub/i386-pc/eltorito.img",
                        "-c",
                        "boot.catalog",
                        "-no-emul-boot",
                        "-boot-load-size",
                        "4",
                        "-boot-info-table",
                        "-o",
                        str(output_path),
                        ".",
                    ]
                    subprocess.run(cmd, capture_output=True, text=True, check=True)
                    success = True
                    logger.info("✅ ISO created with mkisofs (joliet-long)")
                except subprocess.CalledProcessError as e:
                    logger.warning(f"mkisofs with joliet-long failed: {e.stderr}")
                    if output_path.exists():
                        output_path.unlink()

            # Method 3: mkisofs without Joliet (fallback)
            if shutil.which("mkisofs") and not success:
                try:
                    logger.info("Trying mkisofs without Joliet (fallback)...")
                    cmd = [
                        "mkisofs",
                        "-r",
                        "-V",
                        f"Ubuntu 24.04 LTS AUTO",
                        "-cache-inodes",
                        "-l",  # No -J (Joliet) to avoid filename conflicts
                        "-b",
                        "boot/grub/i386-pc/eltorito.img",
                        "-c",
                        "boot.catalog",
                        "-no-emul-boot",
                        "-boot-load-size",
                        "4",
                        "-boot-info-table",
                        "-o",
                        str(output_path),
                        ".",
                    ]
                    subprocess.run(cmd, capture_output=True, text=True, check=True)
                    success = True
                    logger.info("✅ ISO created with mkisofs (no Joliet)")
                except subprocess.CalledProcessError as e:
                    logger.warning(
                        f"mkisofs without Joliet failed: {
                            e.stderr}"
                    )
                    if output_path.exists():
                        output_path.unlink()

            # Method 4: macOS hdiutil
            if shutil.which("hdiutil") and not success:
                try:
                    logger.info("Trying hdiutil (macOS)...")
                    cmd = [
                        "hdiutil",
                        "makehybrid",
                        "-iso",
                        "-joliet",
                        "-o",
                        str(output_path),
                        ".",
                    ]
                    subprocess.run(cmd, capture_output=True, text=True, check=True)
                    success = True
                    logger.info("✅ ISO created with hdiutil")
                except subprocess.CalledProcessError as e:
                    logger.warning(f"hdiutil failed: {e.stderr}")
                    if output_path.exists():
                        output_path.unlink()

            if not success:
                logger.error("All ISO creation methods failed")
                return False

            # Verify the output file was created
            if not output_path.exists():
                logger.error("ISO file was not created despite success message")
                return False

            logger.info(f"ISO rebuilt successfully: {output_path}")
            logger.info(
                f"ISO size: {output_path.stat().st_size / (1024 * 1024):.1f} MB"
            )
            return True

        except Exception as e:
            logger.error(f"Error rebuilding ISO: {e}")
            return False
        finally:
            # Return to original directory
            os.chdir(original_cwd)

    def build_autoinstall_iso(
        self, user_data: str, output_path: Path, ubuntu_version: str = "24.04"
    ) -> bool:
        """Complete ISO build process following the Ubuntu autoinstall guide."""
        logger.info(
            f"🚀 Starting Ubuntu {ubuntu_version} autoinstall ISO build process"
        )

        try:
            # Step 1: Check tools
            if not self.check_tools():
                return False

            # Step 2: Download Ubuntu ISO
            iso_path = self.download_ubuntu_iso(ubuntu_version)

            # Step 3: Extract ISO
            if not self.extract_iso(iso_path):
                return False

            # Step 4: Modify GRUB
            if not self.modify_grub_config():
                return False

            # Step 5: Create autoinstall config
            if not self.create_autoinstall_config(user_data):
                return False

            # Step 6: Rebuild ISO
            if not self.rebuild_iso(output_path):
                return False

            logger.info(f"🎉 Successfully created autoinstall ISO: {output_path}")
            logger.info(f"📁 Work directory: {self.work_dir}")
            return True

        except Exception as e:
            logger.error(f"Failed to build autoinstall ISO: {e}")
            return False

    def cleanup(self):
        """Clean up temporary work directory."""
        if self.work_dir.exists():
            shutil.rmtree(self.work_dir)
            logger.info(f"Cleaned up work directory: {self.work_dir}")


def main():
    """Test the ISO builder."""
    import logging

    logging.basicConfig(level=logging.INFO)

    # Sample autoinstall user-data
    user_data = """#cloud-config
autoinstall:
  version: 1
  packages:
    - ubuntu-server
  identity:
    realname: 'Test User'
    username: testuser
    password: '$6$rounds=4096$saltsalt$[AWS-SECRET-REMOVED]AzpI8g8T14F8VnhXo0sUkZV2NV6/.c77tHgVi34DgbPu.'
    hostname: test-vm
  locale: en_US.UTF-8
  keyboard:
    layout: us
  storage:
    layout:
      name: direct
  ssh:
    install-server: true
  late-commands:
    - curtin in-target -- apt-get autoremove -y
"""

    builder = UbuntuISOBuilder("test-vm")
    output_path = Path("/tmp/ubuntu-24.04-autoinstall.iso")

    success = builder.build_autoinstall_iso(user_data, output_path)
    if success:
        print(f"✅ ISO created: {output_path}")
    else:
        print("❌ ISO creation failed")

    # Optionally clean up
    # builder.cleanup()


if __name__ == "__main__":
    main()
