"""Implements the 'bdist_msi' command (create Windows installer packages)."""

from __future__ import annotations

import contextlib
import logging
import os
import re
import shutil
import sys
import warnings
from typing import ClassVar

from packaging.version import Version
from setuptools import Command

from cx_Freeze._compat import IS_MINGW, IS_WINDOWS, PLATFORM
from cx_Freeze.exception import OptionError, PlatformError

__all__ = ["bdist_msi"]

logger = logging.getLogger(__name__)

if (IS_MINGW or IS_WINDOWS) and sys.version_info[:2] < (3, 13):

    @contextlib.contextmanager
    def suppress_known_deprecation() -> contextlib.AbstractContextManager:
        with warnings.catch_warnings():
            warnings.filterwarnings("ignore", "'msilib' is deprecated")
            yield

    with suppress_known_deprecation():
        from msilib import (  # pylint: disable=deprecated-module
            CAB,
            PID_AUTHOR,
            PID_COMMENTS,
            PID_KEYWORDS,
            Binary,
            Dialog,
            Directory,
            Feature,
            add_data,
            add_tables,
            gen_uuid,
            init_database,
            make_id,
            schema,
            sequence,
        )

        from cx_Freeze.command._pydialog import PyDialog

        # force the remove existing products action to happen first since
        # Windows installer appears to be braindead and doesn't handle files
        # shared between different "products" very well
        install_execute_sequence = sequence.InstallExecuteSequence
        for index, info in enumerate(install_execute_sequence):
            if info[0] == "RemoveExistingProducts":
                install_execute_sequence[index] = (info[0], info[1], 1450)


class bdist_msi(Command):
    """Create a Microsoft Installer (.msi) binary distribution."""

    description = __doc__

    user_options: ClassVar[list[tuple[str, str | None, str]]] = [
        (
            "bdist-dir=",
            None,
            "temporary directory for creating the distribution",
        ),
        ("dist-dir=", "d", "directory to put final built distributions in"),
        (
            "install-script=",
            None,
            "basename of installation script to be run after "
            "installation or before deinstallation",
        ),
        (
            "keep-temp",
            "k",
            "keep the pseudo-installation tree around after "
            "creating the distribution archive",
        ),
        (
            "pre-install-script=",
            None,
            "Fully qualified filename of a script to be run before "
            "any files are installed.  This script need not be in the "
            "distribution",
        ),
        (
            "skip-build",
            None,
            "skip rebuilding everything (for testing/debugging)",
        ),
        # cx_Freeze specific
        ("add-to-path=", None, "add target dir to PATH environment variable"),
        ("all-users=", None, "installation for all users (or just me)"),
        (
            "data=",
            None,
            "dictionary of data indexed by table name, and each value is a "
            "tuple to include in table",
        ),
        ("directories=", None, "list of 3-tuples of directories to create"),
        ("environment-variables=", None, "list of environment variables"),
        ("extensions=", None, "Extensions for which to register Verbs"),
        ("initial-target-dir=", None, "initial target directory"),
        ("install-icon=", None, "icon path to add/remove programs "),
        ("product-code=", None, "product code to use"),
        (
            "summary-data=",
            None,
            "Dictionary of data to include in msi summary data stream. "
            'Allowed keys are "author", "comments", "keywords".',
        ),
        ("target-name=", None, "name of the file to create"),
        ("target-version=", None, "version of the file to create"),
        ("upgrade-code=", None, "upgrade code to use"),
        (
            "license-file=",
            None,
            "rft formatted license file to include in the installer",
        ),
        (
            "launch-on-finish",
            None,
            "Include a Launch on Finish checkbox in the installer.",
        ),
    ]

    boolean_options: ClassVar[list[str]] = [
        "keep-temp",
        "skip-build",
        "launch-on-finish",
    ]

    x = y = 50
    width = 370
    height = 300
    title = "[ProductName] Setup"
    modeless = 1
    modal = 3
    _binary_columns: ClassVar[dict[str, int]] = {
        "Binary": 1,
        "Icon": 1,
        "Patch": 4,
        "SFPCatalog": 1,
        "MsiDigitalCertificate": 1,
        "MsiPatchHeaders": 1,
    }

    def add_config(self) -> None:
        if self.add_to_path:
            path = "Path"
            if self.all_users:
                path = "=-*" + path
            add_data(
                self.db,
                "Environment",
                [("E_PATH", path, r"[~];[TARGETDIR]", "TARGETDIR")],
            )
        if self.directories:
            add_data(self.db, "Directory", self.directories)
        if self.environment_variables:
            add_data(self.db, "Environment", self.environment_variables)
        # This is needed in case the AlwaysInstallElevated policy is set.
        # Otherwise installation will not end up in TARGETDIR.
        add_data(
            self.db,
            "Property",
            [("SecureCustomProperties", "TARGETDIR;REINSTALLMODE")],
        )
        add_data(
            self.db,
            "CustomAction",
            [
                (
                    "A_SET_TARGET_DIR",
                    256 + 51,
                    "TARGETDIR",
                    self.initial_target_dir,
                ),
                (
                    "A_SET_REINSTALL_MODE",
                    256 + 51,
                    "REINSTALLMODE",
                    "amus",
                ),
            ],
        )
        add_data(
            self.db,
            "InstallExecuteSequence",
            [
                ("A_SET_TARGET_DIR", 'TARGETDIR=""', 401),
                ("A_SET_REINSTALL_MODE", 'REINSTALLMODE=""', 402),
            ],
        )
        add_data(
            self.db,
            "InstallUISequence",
            [
                ("PrepareDlg", None, 140),
                ("A_SET_TARGET_DIR", 'TARGETDIR=""', 401),
                ("A_SET_REINSTALL_MODE", 'REINSTALLMODE=""', 402),
                ("SelectDirectoryDlg", "not Installed", 1230),
                ("LicenseAgreementDlg", "not Installed", 1240),
                (
                    "MaintenanceTypeDlg",
                    "Installed and not Resume and not Preselected",
                    1250,
                ),
                ("ProgressDlg", None, 1280),
            ],
        )
        for idx, executable in enumerate(self.distribution.executables):
            if (
                executable.shortcut_name is not None
                and executable.shortcut_dir is not None
            ):
                base_name = os.path.basename(executable.target_name)
                add_data(
                    self.db,
                    "Shortcut",
                    [
                        (
                            f"S_APP_{idx}",
                            os.fspath(executable.shortcut_dir),
                            executable.shortcut_name,
                            "TARGETDIR",
                            f"[TARGETDIR]{base_name}",
                            None,
                            None,
                            None,
                            None,
                            None,
                            None,
                            "TARGETDIR",
                        )
                    ],
                )
        for table_name, table_data in self.data.items():
            col = self._binary_columns.get(table_name)
            if col is not None:
                data = [
                    (*row[:col], Binary(row[col]), *row[col + 1 :])
                    for row in table_data
                ]
            else:
                data = table_data
            add_data(self.db, table_name, data)

        # If provided, add data to MSI's summary information stream
        if len(self.summary_data) > 0:
            for k in self.summary_data:
                if k not in ["author", "comments", "keywords"]:
                    msg = f"Unknown key provided in summary-data: {k!r}"
                    raise OptionError(msg)

            summary_info = self.db.GetSummaryInformation(5)
            if "author" in self.summary_data:
                summary_info.SetProperty(
                    PID_AUTHOR, self.summary_data["author"]
                )
            if "comments" in self.summary_data:
                summary_info.SetProperty(
                    PID_COMMENTS, self.summary_data["comments"]
                )
            if "keywords" in self.summary_data:
                summary_info.SetProperty(
                    PID_KEYWORDS, self.summary_data["keywords"]
                )
            summary_info.Persist()

    def add_cancel_dialog(self) -> None:
        dialog = Dialog(
            self.db,
            "CancelDlg",
            50,
            10,
            260,
            85,
            3,
            self.title,
            "No",
            "No",
            "No",
        )
        dialog.text(
            "Text",
            48,
            15,
            194,
            30,
            3,
            "Are you sure you want to cancel [ProductName] installation?",
        )
        button = dialog.pushbutton("Yes", 72, 57, 56, 17, 3, "Yes", "No")
        button.event("EndDialog", "Exit")
        button = dialog.pushbutton("No", 132, 57, 56, 17, 3, "No", "Yes")
        button.event("EndDialog", "Return")

    def add_error_dialog(self) -> None:
        dialog = Dialog(
            self.db,
            "ErrorDlg",
            50,
            10,
            330,
            101,
            65543,
            self.title,
            "ErrorText",
            None,
            None,
        )
        dialog.text("ErrorText", 50, 9, 280, 48, 3, "")
        for text, pos in [
            ("No", 120),
            ("Yes", 240),
            ("Abort", 0),
            ("Cancel", 42),
            ("Ignore", 81),
            ("Ok", 159),
            ("Retry", 198),
        ]:
            button = dialog.pushbutton(text[0], pos, 72, 81, 21, 3, text, None)
            button.event("EndDialog", f"Error{text}")

    def add_exit_dialog(self) -> None:
        dialog = PyDialog(
            self.db,
            "ExitDialog",
            self.x,
            self.y,
            self.width,
            self.height,
            self.modal,
            self.title,
            "Finish",
            "Finish",
            "Finish",
        )
        dialog.title("Completing the [ProductName] installer")
        if self.launch_on_finish:
            add_data(
                self.db,
                "ControlCondition",
                # Dialog, Control , Action, Condition
                [
                    (
                        "ExitDialog",
                        "LaunchOnFinish",
                        "Hide",
                        'MaintenanceForm_Action="Remove"',
                    )
                ],
            )
            dialog.checkbox(
                "LaunchOnFinish",
                15,
                200,
                300,
                20,
                3,
                "LAUNCHAPP",
                "Launch the installed app on finish?",
                "Finish",
            )
            add_data(
                self.db,
                "ControlEvent",
                # Dialog, Control , Event, Argument, Condition, Ordering
                [
                    (
                        "ExitDialog",
                        "Finish",
                        "DoAction",
                        "VSDCA_Launch",
                        "LAUNCHAPP=1",
                        0,
                    )
                ],
            )
            add_data(
                self.db,
                "CustomAction",
                # Action, Type, Source, Target
                [
                    (
                        "VSDCA_Launch",
                        226,
                        "TARGETDIR",
                        f"[TARGETDIR]\\{self.distribution.executables[0].target_name}",
                    )
                ],
            )
            add_data(
                self.db,
                "Property",
                # Property, Value
                [
                    (
                        "LAUNCHAPP",
                        "1",
                    )
                ],
            )
            dialog.backbutton("< Back", "LaunchOnFinish", active=False)
        else:
            dialog.backbutton("< Back", "Finish", active=False)
        dialog.cancelbutton("Cancel", "Back", active=False)
        dialog.text(
            "Description",
            15,
            235,
            320,
            20,
            0x30003,
            "Click the Finish button to exit the installer.",
        )
        button = dialog.nextbutton("Finish", "Cancel", name="Finish")
        if self.launch_on_finish:
            button.event("EndDialog", "Return", "1", 1)
        else:
            button.event("EndDialog", "Return")

    def add_fatal_error_dialog(self) -> None:
        dialog = PyDialog(
            self.db,
            "FatalError",
            self.x,
            self.y,
            self.width,
            self.height,
            self.modal,
            self.title,
            "Finish",
            "Finish",
            "Finish",
        )
        dialog.title("[ProductName] installer ended prematurely")
        dialog.backbutton("< Back", "Finish", active=False)
        dialog.cancelbutton("Cancel", "Back", active=False)
        dialog.text(
            "Description1",
            15,
            70,
            320,
            80,
            0x30003,
            "[ProductName] setup ended prematurely because of an error. "
            "Your system has not been modified. To install this program "
            "at a later time, please run the installation again.",
        )
        dialog.text(
            "Description2",
            15,
            155,
            320,
            20,
            0x30003,
            "Click the Finish button to exit the installer.",
        )
        button = dialog.nextbutton("Finish", "Cancel", name="Finish")
        button.event("EndDialog", "Exit")

    def add_files(self) -> None:
        database = self.db
        cab = CAB("distfiles")
        feature = Feature(
            database,
            "default",
            "Default Feature",
            "Everything",
            1,
            directory="TARGETDIR",
        )
        feature.set_current()
        rootdir = os.path.abspath(self.bdist_dir)
        root = Directory(
            database, cab, None, rootdir, "TARGETDIR", "SourceDir"
        )
        database.Commit()
        todo = [root]
        while todo:
            directory = todo.pop()
            for file in os.listdir(directory.absolute):
                sep_comp = self.separate_components.get(
                    os.path.relpath(
                        os.path.join(directory.absolute, file),
                        self.bdist_dir,
                    )
                )
                if sep_comp is not None:
                    restore_component = directory.component
                    directory.start_component(
                        component=sep_comp,
                        flags=0,
                        feature=feature,
                        keyfile=file,
                    )
                    directory.add_file(file)
                    directory.component = restore_component
                elif os.path.isdir(os.path.join(directory.absolute, file)):
                    sfile = directory.make_short(file)
                    new_dir = Directory(
                        database, cab, directory, file, file, f"{sfile}|{file}"
                    )
                    todo.append(new_dir)
                else:
                    directory.add_file(file)
        cab.commit(database)

    def add_files_in_use_dialog(self) -> None:
        dialog = PyDialog(
            self.db,
            "FilesInUse",
            self.x,
            self.y,
            self.width,
            self.height,
            19,
            self.title,
            "Retry",
            "Retry",
            "Retry",
            bitmap=False,
        )
        dialog.text(
            "Title", 15, 6, 200, 15, 0x30003, r"{\DlgFontBold8}Files in Use"
        )
        dialog.text(
            "Description",
            20,
            23,
            280,
            20,
            0x30003,
            "Some files that need to be updated are currently in use.",
        )
        dialog.text(
            "Text",
            20,
            55,
            330,
            50,
            3,
            "The following applications are using files that need to be "
            "updated by this setup. Close these applications and then "
            "click Retry to continue the installation or Cancel to exit it.",
        )
        dialog.control(
            "List",
            "ListBox",
            20,
            107,
            330,
            130,
            7,
            "FileInUseProcess",
            None,
            None,
            None,
        )
        button = dialog.backbutton("Exit", "Ignore", name="Exit")
        button.event("EndDialog", "Exit")
        button = dialog.nextbutton("Ignore", "Retry", name="Ignore")
        button.event("EndDialog", "Ignore")
        button = dialog.cancelbutton("Retry", "Exit", name="Retry")
        button.event("EndDialog", "Retry")

    def add_maintenance_type_dialog(self) -> None:
        dialog = PyDialog(
            self.db,
            "MaintenanceTypeDlg",
            self.x,
            self.y,
            self.width,
            self.height,
            self.modal,
            self.title,
            "Next",
            "Next",
            "Cancel",
        )
        dialog.title("Welcome to the [ProductName] Setup Wizard")
        dialog.text(
            "BodyText",
            15,
            63,
            330,
            42,
            3,
            "Select whether you want to repair or remove [ProductName].",
        )
        group = dialog.radiogroup(
            "RepairRadioGroup",
            15,
            108,
            330,
            60,
            3,
            "MaintenanceForm_Action",
            "",
            "Next",
        )
        group.add("Repair", 0, 18, 300, 17, "&Repair [ProductName]")
        group.add("Remove", 0, 36, 300, 17, "Re&move [ProductName]")
        dialog.backbutton("< Back", None, active=False)
        button = dialog.nextbutton("Finish", "Cancel")
        button.event(
            "[REINSTALL]", "ALL", 'MaintenanceForm_Action="Repair"', 5
        )
        button.event(
            "[Progress1]", "Repairing", 'MaintenanceForm_Action="Repair"', 6
        )
        button.event(
            "[Progress2]", "repairs", 'MaintenanceForm_Action="Repair"', 7
        )
        button.event("Reinstall", "ALL", 'MaintenanceForm_Action="Repair"', 8)
        button.event("[REMOVE]", "ALL", 'MaintenanceForm_Action="Remove"', 11)
        button.event(
            "[Progress1]", "Removing", 'MaintenanceForm_Action="Remove"', 12
        )
        button.event(
            "[Progress2]", "removes", 'MaintenanceForm_Action="Remove"', 13
        )
        button.event("Remove", "ALL", 'MaintenanceForm_Action="Remove"', 14)
        button.event(
            "EndDialog", "Return", 'MaintenanceForm_Action<>"Change"', 20
        )
        button = dialog.cancelbutton("Cancel", "RepairRadioGroup")
        button.event("SpawnDialog", "CancelDlg")

    def add_prepare_dialog(self) -> None:
        dialog = PyDialog(
            self.db,
            "PrepareDlg",
            self.x,
            self.y,
            self.width,
            self.height,
            self.modeless,
            self.title,
            "Cancel",
            "Cancel",
            "Cancel",
        )
        dialog.text(
            "Description",
            15,
            70,
            320,
            40,
            0x30003,
            "Please wait while the installer prepares to guide you through "
            "the installation.",
        )
        dialog.title("Welcome to the [ProductName] installer")
        text = dialog.text(
            "ActionText", 15, 110, 320, 20, 0x30003, "Pondering..."
        )
        text.mapping("ActionText", "Text")
        text = dialog.text("ActionData", 15, 135, 320, 30, 0x30003, None)
        text.mapping("ActionData", "Text")
        dialog.backbutton("Back", None, active=False)
        dialog.nextbutton("Next", None, active=False)
        button = dialog.cancelbutton("Cancel", None)
        button.event("SpawnDialog", "CancelDlg")

    def add_progress_dialog(self) -> None:
        dialog = PyDialog(
            self.db,
            "ProgressDlg",
            self.x,
            self.y,
            self.width,
            self.height,
            self.modeless,
            self.title,
            "Cancel",
            "Cancel",
            "Cancel",
            bitmap=False,
        )
        dialog.text(
            "Title",
            20,
            15,
            200,
            15,
            0x30003,
            r"{\DlgFontBold8}[Progress1] [ProductName]",
        )
        dialog.text(
            "Text",
            35,
            65,
            300,
            30,
            3,
            "Please wait while the installer [Progress2] [ProductName].",
        )
        dialog.text("StatusLabel", 35, 100, 35, 20, 3, "Status:")
        text = dialog.text(
            "ActionText", 70, 100, self.width - 70, 20, 3, "Pondering..."
        )
        text.mapping("ActionText", "Text")
        control = dialog.control(
            "ProgressBar",
            "ProgressBar",
            35,
            120,
            300,
            10,
            65537,
            None,
            "Progress done",
            None,
            None,
        )
        control.mapping("SetProgress", "Progress")
        dialog.backbutton("< Back", "Next", active=False)
        dialog.nextbutton("Next >", "Cancel", active=False)
        button = dialog.cancelbutton("Cancel", "Back")
        button.event("SpawnDialog", "CancelDlg")

    def add_properties(self) -> None:
        metadata = self.distribution.metadata
        props = [
            ("DistVersion", metadata.get_version()),
            ("DefaultUIFont", "DlgFont8"),
            ("ErrorDialog", "ErrorDlg"),
            ("Progress1", "Install"),
            ("Progress2", "installs"),
            ("MaintenanceForm_Action", "Repair"),
            ("ALLUSERS", "2"),
        ]

        if not self.all_users:
            props.append(("MSIINSTALLPERUSER", "1"))
        email = metadata.author_email or metadata.maintainer_email
        if email:
            props.append(("ARPCONTACT", email))
        if metadata.url:
            props.append(("ARPURLINFOABOUT", metadata.url))
        if self.upgrade_code is not None:
            if not _is_valid_guid(self.upgrade_code):
                msg = "upgrade-code must be in valid GUID format"
                raise ValueError(msg)
            props.append(("UpgradeCode", self.upgrade_code.upper()))
        if self.install_icon:
            props.append(("ARPPRODUCTICON", "InstallIcon"))
        add_data(self.db, "Property", props)
        if self.install_icon:
            add_data(
                self.db,
                "Icon",
                [("InstallIcon", Binary(self.install_icon))],
            )

    def add_select_directory_dialog(self) -> None:
        dialog = PyDialog(
            self.db,
            "SelectDirectoryDlg",
            self.x,
            self.y,
            self.width,
            self.height,
            self.modal,
            self.title,
            "Next",
            "Next",
            "Cancel",
        )
        dialog.title("Select destination directory")
        dialog.backbutton("< Back", None, active=False)
        button = dialog.nextbutton("Next >", "Cancel")
        button.event("SetTargetPath", "TARGETDIR", ordering=1)
        button.event("SpawnWaitDialog", "WaitForCostingDlg", ordering=2)
        button.event("EndDialog", "Return", ordering=3)
        button = dialog.cancelbutton("Cancel", "DirectoryCombo")
        button.event("SpawnDialog", "CancelDlg")
        dialog.control(
            "DirectoryCombo",
            "DirectoryCombo",
            15,
            70,
            272,
            80,
            393219,
            "TARGETDIR",
            None,
            "DirectoryList",
            None,
        )
        dialog.control(
            "DirectoryList",
            "DirectoryList",
            15,
            90,
            308,
            136,
            3,
            "TARGETDIR",
            None,
            "PathEdit",
            None,
        )
        dialog.control(
            "PathEdit",
            "PathEdit",
            15,
            230,
            306,
            16,
            3,
            "TARGETDIR",
            None,
            "Next",
            None,
        )
        button = dialog.pushbutton("Up", 306, 70, 18, 18, 3, "Up", None)
        button.event("DirectoryListUp", "0")
        button = dialog.pushbutton("NewDir", 324, 70, 30, 18, 3, "New", None)
        button.event("DirectoryListNew", "0")

    def add_text_styles(self) -> None:
        add_data(
            self.db,
            "TextStyle",
            [
                ("DlgFont8", "Tahoma", 9, None, 0),
                ("DlgFontBold8", "Tahoma", 8, None, 1),
                ("VerdanaBold10", "Verdana", 10, None, 1),
                ("VerdanaRed9", "Verdana", 9, 255, 0),
            ],
        )

    def add_ui(self) -> None:
        self.add_text_styles()
        self.add_error_dialog()
        self.add_fatal_error_dialog()
        self.add_cancel_dialog()
        self.add_exit_dialog()
        self.add_user_exit_dialog()
        self.add_files_in_use_dialog()
        self.add_wait_for_costing_dialog()
        self.add_prepare_dialog()
        self.add_license_dialog()
        self.add_select_directory_dialog()
        self.add_progress_dialog()
        self.add_maintenance_type_dialog()

    def add_upgrade_config(self, sversion) -> None:
        if self.upgrade_code is not None:
            add_data(
                self.db,
                "Upgrade",
                [
                    (
                        self.upgrade_code,
                        None,
                        sversion,
                        None,
                        513,
                        None,
                        "REMOVEOLDVERSION",
                    ),
                    (
                        self.upgrade_code,
                        sversion,
                        None,
                        None,
                        257,
                        None,
                        "REMOVENEWVERSION",
                    ),
                ],
            )

    def add_user_exit_dialog(self) -> None:
        dialog = PyDialog(
            self.db,
            "UserExit",
            self.x,
            self.y,
            self.width,
            self.height,
            self.modal,
            self.title,
            "Finish",
            "Finish",
            "Finish",
        )
        dialog.title("[ProductName] installer was interrupted")
        dialog.backbutton("< Back", "Finish", active=False)
        dialog.cancelbutton("Cancel", "Back", active=False)
        dialog.text(
            "Description1",
            15,
            70,
            320,
            80,
            0x30003,
            "[ProductName] setup was interrupted. Your system has not "
            "been modified. To install this program at a later time, "
            "please run the installation again.",
        )
        dialog.text(
            "Description2",
            15,
            155,
            320,
            20,
            0x30003,
            "Click the Finish button to exit the installer.",
        )
        button = dialog.nextbutton("Finish", "Cancel", name="Finish")
        button.event("EndDialog", "Exit")

    def add_license_dialog(self) -> None:
        if self.license_file:
            with open(self.license_file, encoding="utf-8") as file:
                license_text = file.read()

            dialog = PyDialog(
                self.db,
                "LicenseAgreementDlg",
                self.x,
                self.y,
                self.width,
                self.height,
                self.modal,
                self.title,
                "Next",
                "Next",
                "Cancel",
            )
            dialog.title("License Agreement")
            dialog.backbutton("< Back", None, active=False)
            dialog.control(
                name="Text",
                type="ScrollableText",
                x=15,
                y=30,
                w=self.width - 30,
                h=self.height - 100,
                attr=3,
                text=license_text,
                prop=None,
                next=None,
                help=None,
            )

            button = dialog.nextbutton(
                name="Next",
                title="Accept",
                tabnext="Cancel",
                active=0,
            )
            button.event("SpawnWaitDialog", "SelectDirectoryDlg", ordering=1)
            button.event("EndDialog", "Return", ordering=2)

            button = dialog.cancelbutton("Cancel", tabnext="Next")
            button.event("SpawnDialog", "CancelDlg")

            checkbox = dialog.checkbox(
                "LicenseAcceptedCheckbox",
                15,
                230,
                300,
                20,
                3,
                "CheckboxProp",
                "I &accept the terms in the License Agreement",
                None,
            )
            # [property], value
            checkbox.event("[LicenseAcceptedClicked]", "1")

            add_data(
                self.db,
                "ControlCondition",
                # Dialog, Control , Action, Condition
                [
                    (
                        "LicenseAgreementDlg",
                        "Next",
                        "Enable",
                        "LicenseAcceptedClicked",
                    )
                ],
            )

    def add_wait_for_costing_dialog(self) -> None:
        dialog = Dialog(
            self.db,
            "WaitForCostingDlg",
            50,
            10,
            260,
            85,
            self.modal,
            self.title,
            "Return",
            "Return",
            "Return",
        )
        dialog.text(
            "Text",
            48,
            15,
            194,
            30,
            3,
            "Please wait while the installer finishes determining your "
            "disk space requirements.",
        )
        button = dialog.pushbutton(
            "Return", 102, 57, 56, 17, 3, "Return", None
        )
        button.event("EndDialog", "Exit")

    def _append_to_data(self, table, *line) -> None:
        rows = self.data.setdefault(table, [])
        line = tuple(line)
        if line not in rows:
            rows.append(line)

    def initialize_options(self) -> None:
        self.bdist_dir = None
        self.keep_temp = 0
        self.dist_dir = None
        self.skip_build = None
        self.install_script = None
        self.pre_install_script = None
        # cx_Freeze specific
        self.upgrade_code = None
        self.product_code = None
        self.add_to_path = None
        self.initial_target_dir = None
        self.target_name = None
        self.target_version = None
        self.fullname = None
        self.directories = None
        self.environment_variables = None
        self.data = None
        self.summary_data = None
        self.install_icon = None
        self.all_users = False
        self.extensions = None
        self.license_file = None
        self.launch_on_finish = None

    def finalize_options(self) -> None:
        major = sys.version_info.major
        minor = sys.version_info.minor
        if (major, minor) >= (3, 13):
            msg = (
                f"bdist_msi is not supported on Python {major}.{minor} yet.\n"
                "       Please check the pinned issue "
                "https://github.com/marcelotduarte/cx_Freeze/issues/2837"
            )
            raise PlatformError(msg)
        self.set_undefined_options("bdist", ("skip_build", "skip_build"))

        if self.bdist_dir is None:
            bdist_base = self.get_finalized_command("bdist").bdist_base
            self.bdist_dir = os.path.join(bdist_base, "msi")

        self.set_undefined_options("bdist", ("dist_dir", "dist_dir"))

        if self.pre_install_script:
            msg = "the pre-install-script feature is not yet implemented"
            raise OptionError(msg)

        if self.install_script:
            for script in self.distribution.scripts:
                if self.install_script == os.path.basename(script):
                    break
            else:
                msg = (
                    f"install_script '{self.install_script}' not found in "
                    "scripts"
                )
                raise OptionError(msg)
        self.install_script_key = None

        # cx_Freeze specific
        if self.target_name is None:
            self.target_name = self.distribution.get_name()
        if self.target_version is None and self.distribution.metadata.version:
            self.target_version = self.distribution.metadata.version
        name = self.target_name
        version = self.target_version or self.distribution.get_version()
        self.fullname = f"{name}-{version}"
        platform = PLATFORM.replace("win-amd64", "win64")
        if self.initial_target_dir is None:
            if platform == "win64" or platform.startswith("mingw_x86_64"):
                program_files_folder = "ProgramFiles64Folder"
            else:
                program_files_folder = "ProgramFilesFolder"
            self.initial_target_dir = rf"[{program_files_folder}]\{name}"
        if self.add_to_path is None:
            self.add_to_path = False
        if self.directories is None:
            self.directories = []
        if self.environment_variables is None:
            self.environment_variables = []
        if self.license_file is not None:
            self.license_file = os.path.abspath(self.license_file)
        if self.data is None:
            self.data = {}
        if not isinstance(self.summary_data, dict):
            self.summary_data = {}
        self.separate_components = {}
        if self.launch_on_finish is None:
            self.launch_on_finish = False
        for idx, executable in enumerate(self.distribution.executables):
            base_name = os.path.basename(executable.target_name)
            # Trying to make these names unique from any directory name
            self.separate_components[base_name] = make_id(
                f"_cx_executable{idx}_{executable}"
            )
        if self.extensions is None:
            self.extensions = []
        for extension in self.extensions:
            try:
                ext = extension["extension"]
                verb = extension["verb"]
                executable = extension["executable"]
            except KeyError:
                msg = (
                    "Each extension must have at least extension, verb, "
                    "and executable"
                )
                raise ValueError(msg) from None
            try:
                component = self.separate_components[executable]
            except KeyError:
                msg = (
                    "Executable must be the base target name of one of the "
                    "distribution's executables"
                )
                raise ValueError(msg) from None
            stem = os.path.splitext(executable)[0]
            progid = make_id(f"{name}.{stem}.{version}")
            mime = extension.get("mime", None)
            # "%1" a better default for argument?
            argument = extension.get("argument", None)
            context = extension.get("context", f"{self.fullname} {verb}")
            # Add rows via self.data to safely ignore duplicates
            self._append_to_data(
                "ProgId",
                progid,
                None,
                None,
                self.distribution.get_description() or "UNKNOWN",
                None,
                None,
            )
            self._append_to_data(
                "Extension", ext, component, progid, mime, "default"
            )
            self._append_to_data("Verb", ext, verb, 0, context, argument)
            if mime is not None:
                self._append_to_data("MIME", mime, ext, "None")
            # Registry entries that allow proper display of the app in menu
            self._append_to_data(
                "Registry",
                f"{progid}-name",
                -1,
                rf"Software\Classes\{progid}",
                "FriendlyAppName",
                name,
                component,
            )
            self._append_to_data(
                "Registry",
                f"{progid}-verb-{verb}",
                -1,
                rf"Software\Classes\{progid}\shell\{verb}",
                "FriendlyAppName",
                name,
                component,
            )
            self._append_to_data(
                "Registry",
                f"{progid}-author",
                -1,
                rf"Software\Classes\{progid}\Application",
                "ApplicationCompany",
                self.distribution.get_author() or "UNKNOWN",
                component,
            )

    def run(self) -> None:
        if not self.skip_build:
            self.run_command("build")

        # install everything from build directory in a new prefix
        install_dir = self.bdist_dir
        install = self.reinitialize_command("install", reinit_subcommands=1)
        install.prefix = install_dir
        install.skip_build = self.skip_build
        install.warn_dir = 0
        logger.info("installing to %s", install_dir)
        install.ensure_finalized()
        install.run()

        # make msi (by default in dist directory)
        self.mkpath(self.dist_dir)
        platform = PLATFORM.replace("win-amd64", "win64")

        msi_name: str
        if os.path.splitext(self.target_name)[1].lower() == ".msi":
            msi_name = self.target_name
        elif self.target_version:
            msi_name = f"{self.fullname}-{platform}.msi"
        else:
            msi_name = f"{self.target_name}-{platform}.msi"
        installer_name = os.path.join(self.dist_dir, msi_name)
        installer_name = os.path.abspath(installer_name)
        if os.path.exists(installer_name):
            os.unlink(installer_name)

        author = self.distribution.metadata.get_contact() or "UNKNOWN"
        version = self.target_version or self.distribution.get_version()
        # ProductVersion must be strictly numeric
        base_version = Version(version).base_version

        # msilib is reloaded in order to reset the "_directories" global member
        # in that module.  That member is used by msilib to prevent any two
        # directories from having the same logical name.  _directories might
        # already have contents due to msilib having been previously used in
        # the current instance of the python interpreter -- if so, it could
        # prevent the root from getting the logical name TARGETDIR, breaking
        # the MSI.
        # importlib.reload(msilib)

        if self.product_code is None:
            self.product_code = gen_uuid()
        self.db = init_database(
            installer_name,
            schema,
            self.target_name,
            self.product_code,
            base_version,
            author,
        )
        add_tables(self.db, sequence)
        self.add_properties()
        self.add_config()
        self.add_upgrade_config(base_version)
        self.add_ui()
        self.add_files()
        self.db.Commit()
        self.distribution.dist_files.append(
            ("bdist_msi", base_version or "any", self.target_name)
        )

        if not self.keep_temp:
            logger.info("removing '%s' (and everything under it)", install_dir)
            if not self.dry_run:
                try:
                    shutil.rmtree(install_dir)
                except OSError as exc:
                    logger.warning("error removing %s: %s", install_dir, exc)

        # Cause the MSI file to be released. Without this, then if bdist_msi
        # is run programmatically from within a larger script, subsequent
        # editing of the MSI is blocked.
        self.db = None


def _is_valid_guid(code) -> bool:
    pattern = re.compile(
        r"^\{[0-9A-F]{8}-([0-9A-F]{4}-){3}[0-9A-F]{12}\}$", re.IGNORECASE
    )
    return isinstance(code, str) and pattern.match(code) is not None
