"""Build script for Aura EXE using Nuitka."""

from __future__ import annotations

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

# Configuration

APP_NAME = "Aura"
PACKAGE_NAME = "aura"

ICON_PATH = "media/AurA.ico"
MEDIA_DIR = "media"
OUTPUT_DIR = "build"
DEFAULT_NUITKA_JOBS = max(1, (os.cpu_count() or 2) // 2)

FINAL_DIST_NAME = f"{APP_NAME}.dist"
FINAL_EXE_NAME = f"{APP_NAME}.exe"
ZIP_NAME = "Aura-Windows-x64.zip"
UPDATER_HELPER_SOURCE = Path(PACKAGE_NAME) / "windows_updater.cmd"
UPDATER_HELPER_DIST_NAME = "AuraUpdater.cmd"

REQUIRED_MEDIA_FILES = [
    "account_tree_.svg",
    "arrow_forward_24dp.svg",
    "AurA.ico",
    "Aura-Working.mp4",
    "commit.svg",
    "diff-view.png",
    "dispatch.png",
    "file-change-dialog.png",
    "file_24.svg",
    "folder_24.svg",
    "fork_right.svg",
    "mermaid.min.js",
    "new_conv.svg",
    "open_conversation.svg",
    "plan_and_code.gif",
    "read_only.svg",
    "settings_24dp.svg",
    "token-cost.png",
    "workflow-complete.png",
    "working.png",
]

# Signing Configuration
SIGN_CERT = os.environ.get("AURA_SIGN_CERT")
SIGN_PASS = os.environ.get("AURA_SIGN_PASS")


# Installer Configuration
INSTALLER_ISS_PATH = "scripts/installer/Aura.iss"
INSTALLER_BASE_NAME = "AuraSetup"


# Version Management

VERSION_PATTERN = re.compile(r"^\d+\.\d+\.\d+$")


def normalize_version(version: str) -> str:
    """Normalize a version string and require X.Y.Z format."""
    clean_version = version.strip().lstrip("vV")
    if not VERSION_PATTERN.fullmatch(clean_version):
        raise SystemExit("Invalid version. Expected X.Y.Z or vX.Y.Z, for example 1.3.4.")
    return clean_version


def read_current_version(root: Path) -> str:
    """Read the current package version from aura/version.py."""
    version_file = root / "aura" / "version.py"
    content = version_file.read_text(encoding="utf-8")
    match = re.search(r'__version__ = "([^"]+)"', content)
    if not match:
        raise SystemExit(f"Could not find __version__ in {version_file}")
    return normalize_version(match.group(1))


def write_text_if_changed(path: Path, content: str) -> None:
    """Write text only when the resulting file content changes."""
    if path.read_text(encoding="utf-8") != content:
        path.write_text(content, encoding="utf-8")


def update_files(root: Path, new_version: str) -> None:
    """Update version strings in all required files."""
    # 1. aura/version.py
    version_file = root / "aura" / "version.py"
    v_content = version_file.read_text(encoding="utf-8")
    write_text_if_changed(
        version_file,
        re.sub(r'__version__ = "[^"]+"', f'__version__ = "{new_version}"', v_content),
    )

    # 2. pyproject.toml
    toml_file = root / "pyproject.toml"
    t_content = toml_file.read_text(encoding="utf-8")
    write_text_if_changed(
        toml_file,
        re.sub(r'^version = "[^"]+"', f'version = "{new_version}"', t_content, flags=re.MULTILINE),
    )

    # 3. README.md
    readme_file = root / "README.md"
    r_content = readme_file.read_text(encoding="utf-8")
    write_text_if_changed(
        readme_file,
        re.sub(r"badge/version-([\d.]+)-orange", f"badge/version-{new_version}-orange", r_content),
    )

    print(f"Version updated to {new_version} in version.py, pyproject.toml, and README.md")


# Helpers


def run(cmd: list[str]) -> None:
    """Run a subprocess command and fail loudly if it errors."""
    print(f"Running: {' '.join(cmd)}")
    subprocess.run(cmd, check=True)


def create_build_venv(root: Path) -> Path:
    """Create a pristine virtual environment for the build."""
    venv_dir = root / OUTPUT_DIR / ".build_venv"
    if venv_dir.exists():
        print("Cleaning up old build environment...")
        shutil.rmtree(venv_dir, ignore_errors=True)

    print("Creating pristine build environment...")
    import venv

    venv.create(venv_dir, with_pip=True)

    python_exe = venv_dir / "Scripts" / "python.exe"
    if not python_exe.exists():
        raise SystemExit(f"Failed to find python executable in {venv_dir}")

    print("Installing Aura and build dependencies into pristine environment...")
    run([str(python_exe), "-m", "pip", "install", "-e", ".", "nuitka", "zstandard"])

    return python_exe


def validate_project_paths(root: Path) -> None:
    """Validate required project paths before starting."""
    required_paths = [
        root / PACKAGE_NAME,
        root / PACKAGE_NAME / "__main__.py",
        root / UPDATER_HELPER_SOURCE,
        root / ICON_PATH,
        root / MEDIA_DIR,
    ]
    missing = [path for path in required_paths if not path.exists()]
    media_dir = root / MEDIA_DIR
    missing.extend(media_dir / filename for filename in REQUIRED_MEDIA_FILES if not (media_dir / filename).is_file())
    if missing:
        details = "\n".join(f"  - {path}" for path in missing)
        raise SystemExit(f"Missing required build files:\n{details}")


def clean_previous_dist_dirs(root: Path) -> None:
    """Remove stale Nuitka dist folders."""
    build_dir = root / OUTPUT_DIR
    build_dir.mkdir(parents=True, exist_ok=True)
    for dist_dir in build_dir.glob("*.dist"):
        shutil.rmtree(dist_dir, ignore_errors=True)


def find_created_dist_dir(root: Path) -> Path:
    """Find the dist folder Nuitka created."""
    build_dir = root / OUTPUT_DIR
    candidates = sorted(build_dir.glob("*.dist"), key=lambda p: p.stat().st_mtime, reverse=True)
    for dist_dir in candidates:
        if (dist_dir / FINAL_EXE_NAME).exists():
            return dist_dir
    print("Could not find dist folder with executable.")
    sys.exit(1)


def normalize_dist_dir(root: Path, created_dist_dir: Path) -> Path:
    """Ensure final dist folder is build/Aura.dist."""
    final_dist_dir = root / OUTPUT_DIR / FINAL_DIST_NAME
    if created_dist_dir.resolve() != final_dist_dir.resolve():
        if final_dist_dir.exists():
            shutil.rmtree(final_dist_dir, ignore_errors=True)
        created_dist_dir.rename(final_dist_dir)
    return final_dist_dir


def zip_distribution(root: Path, final_dist_dir: Path) -> Path:
    """Package Aura.dist into a ZIP."""
    zip_base = root / OUTPUT_DIR / "Aura-Windows-x64"
    print(f"Creating release archive: {zip_base}.zip")
    shutil.make_archive(str(zip_base), "zip", root_dir=str(final_dist_dir), base_dir=".")
    return Path(f"{zip_base}.zip")


def copy_to_desktop(zip_path: Path) -> None:
    """Copy the final ZIP to the user's desktop, handling OneDrive redirects."""
    home = Path.home()

    # Try common desktop locations, prioritizing OneDrive
    candidates = [
        home / "OneDrive" / "Desktop",
        home / "Desktop",
    ]

    target_desktop = None
    for cand in candidates:
        if cand.exists():
            target_desktop = cand
            break

    if target_desktop:
        target = target_desktop / ZIP_NAME
        shutil.copy2(zip_path, target)
        print(f"Success! Release ZIP copied to: {target}")
    else:
        print("Could not find Desktop folder to copy the release ZIP.")


def find_iscc() -> Path | None:
    """Find Inno Setup's iscc.exe compiler."""
    exe = shutil.which("iscc")
    if exe:
        return Path(exe)
    candidates = [
        "C:\\Program Files (x86)\\Inno Setup 6\\iscc.exe",
        "C:\\Program Files\\Inno Setup 6\\iscc.exe",
        "C:\\Program Files (x86)\\Inno Setup\\iscc.exe",
        "C:\\Program Files\\Inno Setup\\iscc.exe",
    ]
    for candidate in candidates:
        path = Path(candidate)
        if path.exists():
            return path
    return None


def create_installer(dist_dir: Path, version: str, installer_flag: bool | None) -> Path | None:
    """Build an Inno Setup installer from the dist directory."""
    if installer_flag is False:
        print("Skipping installer creation (--no-installer).")
        return None

    iscc = find_iscc()
    if installer_flag is True and iscc is None:
        raise SystemExit(
            "Cannot create installer: iscc.exe not found. Install Inno Setup 6 or ensure iscc.exe is on PATH."
        )

    if iscc is None:
        print("iscc.exe not found. Skipping installer creation.")
        print("Install Inno Setup 6 to enable installer builds.")
        return None

    root = Path(__file__).resolve().parent.parent
    iss_path = root / INSTALLER_ISS_PATH
    if not iss_path.exists():
        raise SystemExit(f"Installer script not found: {iss_path}")

    output_dir = root / OUTPUT_DIR
    output_dir.mkdir(parents=True, exist_ok=True)

    cmd = [
        str(iscc),
        f"/DMyAppVersion={version}",
        f"/DSourceDir={dist_dir}",
        str(iss_path),
    ]
    run(cmd)

    installer_path = output_dir / f"{INSTALLER_BASE_NAME}-{version}.exe"
    if not installer_path.exists():
        raise SystemExit(
            f"Expected installer not found: {installer_path}\n"
            "Check that the ISS script outputs to the expected location."
        )

    print(f"Installer created: {installer_path}")
    return installer_path


def create_nuitka_command(
    python_exe: Path | None = None,
    *,
    low_memory: bool = True,
    jobs: int = DEFAULT_NUITKA_JOBS,
) -> list[str]:
    """Create the Nuitka command used for release builds."""
    if jobs == 0:
        raise SystemExit("--jobs cannot be 0.")
    python_exe = python_exe or Path(sys.executable)

    cmd = [
        str(python_exe),
        "-m",
        "nuitka",
        "--standalone",
        "--enable-plugin=pyside6",
        "--windows-console-mode=disable",
        f"--windows-icon-from-ico={ICON_PATH}",
        f"--include-data-dir={MEDIA_DIR}={MEDIA_DIR}",
        "--include-package=aura",
        f"--include-data-file={UPDATER_HELPER_SOURCE}={UPDATER_HELPER_DIST_NAME}",
        f"--output-dir={OUTPUT_DIR}",
        f"--output-filename={APP_NAME}",
        "--clean-cache=all",
        "--assume-yes-for-downloads",
        "--python-flag=-m",
        "--nofollow-import-to=google",
        "--nofollow-import-to=libcst",
        "--nofollow-import-to=numpy",
        "--nofollow-import-to=scipy",
        "--nofollow-import-to=pytest",
        "--nofollow-import-to=charset_normalizer",
        "--nofollow-import-to=click",
        "--lto=no",
    ]
    if low_memory:
        cmd.append("--low-memory")
    cmd.append(f"--jobs={jobs}")

    if SIGN_CERT:
        cmd.append(f"--windows-sign-certificate={SIGN_CERT}")
        if SIGN_PASS:
            cmd.append(f"--windows-sign-certificate-password={SIGN_PASS}")

    cmd.append(PACKAGE_NAME)
    return cmd


# Main Build Flow


def resolve_build_version(
    root: Path,
    requested_version: str | None,
    *,
    skip_version_update: bool = False,
) -> str:
    """Resolve the build version, updating files only when explicitly requested."""
    if requested_version is not None and skip_version_update:
        raise SystemExit("--version and --skip-version-update cannot be used together.")

    if requested_version is not None:
        version = normalize_version(requested_version)
        update_files(root, version)
        return version

    if skip_version_update:
        version = read_current_version(root)
        print(f"Using current version: {version}")
        return version

    # Default to interactive if not skipping and no version provided
    current = read_current_version(root)
    raw = input(f"Enter release version X.Y.Z or leave blank to keep {current}: ").strip()
    if raw:
        version = normalize_version(raw)
        update_files(root, version)
        return version

    print(f"Using current version: {current}")
    return current


def build(
    version: str | None = None,
    *,
    skip_version_update: bool = False,
    copy_desktop: bool = True,
    low_memory: bool = True,
    jobs: int = DEFAULT_NUITKA_JOBS,
    installer: bool | None = None,
) -> None:
    root = Path(__file__).resolve().parent.parent
    os.chdir(root)

    # 1. Versioning
    new_version = resolve_build_version(
        root,
        version,
        skip_version_update=skip_version_update,
    )

    # 2. Validation & Cleanup
    validate_project_paths(root)
    clean_previous_dist_dirs(root)

    # 3. Build Environment
    python_exe = create_build_venv(root)

    # 4. Nuitka Command
    cmd = create_nuitka_command(python_exe, low_memory=low_memory, jobs=jobs)

    # 5. Run Build
    print(f"\nStarting Nuitka build for version {new_version}...")
    try:
        run(cmd)
    except subprocess.CalledProcessError:
        print("\nBuild failed.")
        sys.exit(1)

    # 6. Package & Deploy
    dist_dir = find_created_dist_dir(root)
    final_dist_dir = normalize_dist_dir(root, dist_dir)

    # Helper to find a package path inside the clean venv
    def get_venv_package_path(pkg: str) -> Path | None:
        try:
            out = subprocess.check_output(
                [str(python_exe), "-c", f"import {pkg}; print({pkg}.__file__)"], text=True, stderr=subprocess.DEVNULL
            ).strip()
            return Path(out).resolve().parent
        except subprocess.CalledProcessError:
            return None

    # Copy google-genai as pure Python files to avoid Nuitka compilation hang/crash
    google_genai_path = get_venv_package_path("google.genai")
    if google_genai_path and google_genai_path.exists():
        target_google_dir: Path = final_dist_dir / "google"
        target_genai_dir: Path = target_google_dir / "genai"
        print(f"Bundling google-genai as raw source: {google_genai_path} -> {target_genai_dir}")
        if target_genai_dir.exists():
            shutil.rmtree(target_genai_dir)
        shutil.copytree(google_genai_path, target_genai_dir)
    else:
        print("Warning: google-genai is not installed in the clean environment, skipping manual bundle.")

    # Copy libcst as pure Python files to avoid Nuitka compilation hang/crash
    libcst_path = get_venv_package_path("libcst")
    if libcst_path and libcst_path.exists():
        target_libcst_dir: Path = final_dist_dir / "libcst"
        print(f"Bundling libcst as raw source: {libcst_path} -> {target_libcst_dir}")
        if target_libcst_dir.exists():
            shutil.rmtree(target_libcst_dir)
        shutil.copytree(libcst_path, target_libcst_dir)
    else:
        print("Warning: libcst is not installed in the clean environment, skipping manual bundle.")

    # Copy charset_normalizer as pure Python files to avoid Nuitka compilation hang/crash
    charset_normalizer_path = get_venv_package_path("charset_normalizer")
    if charset_normalizer_path and charset_normalizer_path.exists():
        target_charset_dir: Path = final_dist_dir / "charset_normalizer"
        print(f"Bundling charset_normalizer as raw source: {charset_normalizer_path} -> {target_charset_dir}")
        if target_charset_dir.exists():
            shutil.rmtree(target_charset_dir)
        shutil.copytree(charset_normalizer_path, target_charset_dir)
    else:
        print("Warning: charset_normalizer is not installed in the clean environment, skipping manual bundle.")

    # Copy click as pure Python files to avoid Nuitka C compiler crash
    click_path = get_venv_package_path("click")
    if click_path and click_path.exists():
        target_click_dir: Path = final_dist_dir / "click"
        print(f"Bundling click as raw source: {click_path} -> {target_click_dir}")
        if target_click_dir.exists():
            shutil.rmtree(target_click_dir)
        shutil.copytree(click_path, target_click_dir)
    else:
        print("Warning: click is not installed in the clean environment, skipping manual bundle.")

    zip_path = zip_distribution(root, final_dist_dir)
    if copy_desktop:
        copy_to_desktop(zip_path)

    installer_path = create_installer(final_dist_dir, new_version, installer)
    if installer_path:
        print(f"Installer created at: {installer_path}")


def parse_args(argv: list[str]) -> argparse.Namespace:
    """Parse command-line arguments."""
    parser = argparse.ArgumentParser(description="Build Aura with Nuitka.")
    parser.add_argument(
        "--version",
        help=("Set project version before building. When omitted, the user is prompted to enter a version (default)."),
    )
    parser.add_argument(
        "--skip-version-update",
        action="store_true",
        help="Use the current project version without prompting or editing files.",
    )
    parser.add_argument(
        "--no-copy-desktop",
        action="store_true",
        help="Do not copy the release ZIP to the Desktop after packaging.",
    )
    parser.add_argument(
        "--jobs",
        type=int,
        default=DEFAULT_NUITKA_JOBS,
        help=(
            "Number of parallel C compiler jobs for Nuitka. Defaults to 1 to "
            "avoid MSVC heap exhaustion on large generated modules."
        ),
    )
    parser.add_argument(
        "--no-low-memory",
        action="store_true",
        help="Disable Nuitka low-memory mode. This may be faster but can trigger MSVC heap exhaustion.",
    )
    parser.add_argument(
        "--installer",
        action="store_true",
        default=None,
        help="Enable installer creation. Auto-detects if iscc.exe is available.",
    )
    parser.add_argument(
        "--no-installer",
        action="store_true",
        help="Explicitly skip installer creation.",
    )
    return parser.parse_args(argv)


if __name__ == "__main__":
    args = parse_args(sys.argv[1:])

    installer: bool | None = None
    if args.installer:
        installer = True
    if args.no_installer:
        installer = False

    build(
        args.version,
        skip_version_update=args.skip_version_update,
        copy_desktop=not args.no_copy_desktop,
        low_memory=not args.no_low_memory,
        jobs=args.jobs,
        installer=installer,
    )
