import os
from io import StringIO
from pathlib import Path
import datetime
from jinja2 import Template

from conan import ConanFile
from conan.tools.env import VirtualRunEnv, Environment
from conan.tools.scm import Version
from conan.tools.files import copy, save, mkdir

required_conan_version = ">=1.58"


class ClientConan(ConanFile):
    name = "client"
    license = "LGPL-3.0"
    author = "Ultimaker B.V. modified by BigRep GmbH"
    url = "https://gitlab.bigrep.com/blade/blade_5"
    description = "3D printer / slicing GUI built on top of the Uranium framework"
    topics = ("conan", "python", "pyqt5", "qt", "qml", "3d-printing", "slicer")
    build_policy = "missing"
    exports = "LICENSE*", "BladeVersion.py.jinja", "*.jinja"
    settings = "os", "compiler", "build_type", "arch"

    python_requires = "conan-common/2.1.2@bigrep/alpha"
    python_requires_extend = "conan-common.ConanCommonBase"
    generators = "VirtualPythonEnv"  # creates the venv

    options = {
        "devtools": [True, False],  # FIXME: Split this up in testing and (development / build (pyinstaller) / system installer) tools
        "display_name": "ANY",  # TODO: should this be an option??
        "internal": [True, False],
        "debug_mode": [True, False],
    }
    default_options = {
        "devtools": False,
        "display_name": "Blade",
        "internal": True,
        "debug_mode": False,
    }

    @property
    def _pycharm_targets(self):
        return self.conan_data["generators"]["pycharm_targets"]

    # FIXME: These env vars should be defined in the runenv.
    _cura_env = None

    @property
    def _cura_run_env(self):
        if self._cura_env:
            return self._cura_env

        self._cura_env = Environment()
        self._cura_env.define("QML2_IMPORT_PATH", str(self._site_packages.joinpath("PyQt6", "Qt6", "qml")))
        self._cura_env.define("QT_PLUGIN_PATH", str(self._site_packages.joinpath("PyQt6", "Qt6", "plugins")))

        if not self.in_local_cache:
            self._cura_env.define("CURA_DATA_ROOT", str(self._share_dir.joinpath("cura")))

        return self._cura_env

    @property
    def _app_name(self):
        match self.channel:
            case "external" if not self.options.internal:
                return str(self.options.display_name)
            case "external" | "internal" | "fleet" | "alpha":
                return f"{self.options.display_name}_{self.channel}"
            case _:
                return f"{self.options.display_name}_test"

    @property
    def requirements_txts(self):
        if self.options.devtools:
            return ["requirements.txt", "requirements-ultimaker.txt", "requirements-dev.txt"]
        return ["requirements.txt"]

    @property
    def _base_dir(self):
        if self.install_folder is None:
            if self.build_folder is not None:
                return Path(self.build_folder)
            return Path(os.getcwd(), "venv")
        if self.in_local_cache:
            return Path(self.install_folder)
        else:
            return Path(self.source_folder, "venv")

    @property
    def _share_dir(self):
        return self._base_dir.joinpath("share")

    @property
    def _script_dir(self):
        if self.settings.os == "Windows":
            return self._base_dir.joinpath("Scripts")
        return self._base_dir.joinpath("bin")

    @property
    def _site_packages(self): 
        # this should be do-able using the properties defined in python-for-blade
        if self.settings.os == "Windows":
            return self._base_dir.joinpath("Lib", "site-packages")
        py_version = Version(self.deps_cpp_info["python-for-blade"].version)
        return self._base_dir.joinpath("lib", f"python{py_version.major}.{py_version.minor}", "site-packages")

    @property
    def _py_interp(self):
        py_interp = self._script_dir.joinpath(Path(self.deps_user_info["python-for-blade"].python).name)
        if self.settings.os == "Windows":
            py_interp = Path(*[f'"{p}"' if " " in p else p for p in py_interp.parts])
        return py_interp

    def validate(self):
        """Basic validations to ensure sanity.
        At present the follow validations exist:
        - ensure all resources paths are strings, if they are pathlib it breaks pyinstaller
        """
        for pkg_name in self.deps_user_info:
            for resource_dir in self.deps_cpp_info[pkg_name].resdir:
                if not isinstance(pkg_path, str):
                    raise ConanInvalidConfiguration(f"The resource dir: {resource_dir} is not of type str in: {pkg}. Please fix immediately")

    def _conan_installs(self):
        """Usefull for freeze installations and debugging, places information in BladeVersion.py"""
        self.output.info("Collecting conan installs")
        conan_installs = {}

        # list of conan installs
        for dependency in self.dependencies.host.values():
            conan_installs[dependency.ref.name] = {
                "version": dependency.ref.version,
                "revision": dependency.ref.revision
            }
        return conan_installs

    def _python_installs(self):
        """Usefull for freeze installations and debugging, places information in BladeVersion.py"""
        self.output.info("Collecting python installs")
        python_installs = {}

        # list of python installs
        run_env = VirtualRunEnv(self)
        env = run_env.environment()
        env.prepend_path("PYTHONPATH", str(self._site_packages.as_posix()))
        venv_vars = env.vars(self, scope = "run")

        outer = '"' if self.settings.os == "Windows" else "'"
        inner = "'" if self.settings.os == "Windows" else '"'
        buffer = StringIO()
        with venv_vars.apply():
            self.run(f"""python -c {outer}import pkg_resources;  print({inner};{inner}.join([(s.key+{inner},{inner}+ s.version) for s in pkg_resources.working_set])){outer}""",
                          env = "conanrun",
                          output = buffer)

        packages = str(buffer.getvalue()).split("-----------------\n")
        packages = packages[1].strip('\r\n').split(";")
        for package in packages:
            name, version = package.split(",")
            python_installs[name] = {"version": version}

        return python_installs

    def system_requirements(self):
        # to avoid installing sip
        pass


    @property
    def _full_verison_str(self) -> str:
        """This is a version string with any patch information removed and the build ID, version.micro, after a -"""
        version = Version(self.version)
        return f"{version.major}.{version.minor}.{version.patch}-{version.micro}"

    def _generate_version(self, location):
        """Generate a valid version number and `BaldeVersion.py` file"""
        with open(Path(__file__).parent.joinpath("BladeVersion.py.jinja"), "r") as f:
            version_py = Template(f.read())

        # If you want a specific version to show up on the splash screen add the user configuration `user.blade:version=VERSION`
        # the global.conf, profile, package_info (of dependency) or via the cmd line `-c user.blade:version=VERSION`

        # name and URl
        base_name = "Blade"
        if self.channel == "external":
            name = f"BigRep {base_name}"
        elif self.channel == "alpha":
            name = f"{base_name} (alpha)"
        elif self.channel == "fleet":
            name = f"{base_name} (fleet)"
        else:
            name = f"{base_name} (test)"

        with open(Path(location, "BladeVersion.py"), "w") as f:
            f.write(version_py.render(
                app_name=name, # this is how updates are checked
                app_display_name=self._app_name,
                version=self._full_verison_str,
                build_type=self.channel,
                cura_debug_mode=self.options.debug_mode,
                # TODO: have different latest.json url depending on if release and stable or internal versions
                latest_url="https://bigrep.com/wp-content/uploads/Blade/latest.json",
                conan_installs=self._conan_installs(),
                python_installs=self._python_installs(),
                ))

    def _generate_pyinstaller_spec(self, location, pyinstaller_metadata, entitlements_file):
        datas = [(str(self._base_dir.joinpath("conan_install_info.json")), ".")]

        p = pyinstaller_metadata["runinfo"]["entrypoint"]
        entrypoint_location = f"'{p}'"
        p = pyinstaller_metadata["icon"][str(self.settings.os)]
        icon_path = f"'{p}'"

        for data in pyinstaller_metadata["datas"].values():
            if not self.options.internal and data.get("internal", False):
                continue

            if "package" in data:  # get the paths from conan package
                if data["package"] == self.name:
                    if self.in_local_cache:
                        src_path = os.path.join(self.package_folder, data["src"])
                    else:
                        src_path = os.path.join(self.source_folder, data["src"])
                else:
                    src_path = os.path.join(self.deps_cpp_info[data["package"]].rootpath, data["src"])
            elif "root" in data:  # get the paths relative from the sourcefolder
                src_path = os.path.join(self.source_folder, data["root"], data["src"])
            else:
                continue
            if Path(src_path).exists():
                datas.append((str(src_path), data["dst"]))

        binaries = []
        for binary in pyinstaller_metadata["binaries"].values():
            if "package" in binary:  # get the paths from conan package
                src_path = os.path.join(self.deps_cpp_info[binary["package"]].rootpath, binary["src"])
            elif "root" in binary:  # get the paths relative from the sourcefolder
                src_path = os.path.join(self.source_folder, binary["root"], binary["src"])
            else:
                continue
            if not Path(src_path).exists():
                self.output.warning(f"Source path for binary {binary['binary']} does not exist")
                continue

            for bin in Path(src_path).glob(binary["binary"] + "*[.exe|.dll|.so|.dylib|.so.]*"):
                binaries.append((str(bin), binary["dst"]))
            for bin in Path(src_path).glob(binary["binary"]):
                binaries.append((str(bin), binary["dst"]))

        # Make sure all Conan dependencies which are shared are added to the binary list for pyinstaller
        for _, dependency in self.dependencies.host.items():
            for bin_paths in dependency.cpp_info.bindirs:
                binaries.extend([(f"{p}", ".") for p in Path(bin_paths).glob("**/*.dll")])
            for lib_paths in dependency.cpp_info.libdirs:
                binaries.extend([(f"{p}", ".") for p in Path(lib_paths).glob("**/*.so*")])
                binaries.extend([(f"{p}", ".") for p in Path(lib_paths).glob("**/*.dylib*")])

        # Copy dynamic libs from lib path
        binaries.extend([(f"{p}", ".") for p in Path(self._base_dir.joinpath("lib")).glob("**/*.dylib*")])
        binaries.extend([(f"{p}", ".") for p in Path(self._base_dir.joinpath("lib")).glob("**/*.so*")])

        # Collect all dll's from PyQt6 and place them in the root
        binaries.extend([(f"{p}", ".") for p in Path(self._site_packages, "PyQt6", "Qt6").glob("**/*.dll")])

        with open(os.path.join(self.recipe_folder, "Blade.spec.jinja"), "r") as f:
            pyinstaller = Template(f.read())

        # version = self.conf_info.get("user.cura:version", default = self.version, check_type = str)
        blade_version = Version(self.version)

        with open(os.path.join(location, "Blade.spec"), "w") as f:
            f.write(pyinstaller.render(
                name = str(self.options.display_name).replace(" ", "-"),
                display_name = self._app_name,
                entrypoint = entrypoint_location,
                datas = datas,
                binaries = binaries,
                venv_script_path = str(self._script_dir),
                hiddenimports = pyinstaller_metadata["hiddenimports"],
                collect_all = pyinstaller_metadata["collect_all"],
                icon = icon_path,
                entitlements_file = entitlements_file,
                osx_bundle_identifier = "'nl.ultimaker.cura'" if self.settings.os == "Macos" else "None",
                upx = str(self.settings.os == "Windows"),
                strip = False,  # This should be possible on Linux and MacOS but, it can also cause issues on some distributions. Safest is to disable it for now
                target_arch = "'x86_64'" if self.settings.os == "Macos" else "None",  # FIXME: Make this dependent on the settings.arch_target
                macos = self.settings.os == "Macos",
                version = self._full_verison_str,
                short_version = f"'{blade_version.major}.{blade_version.minor}'",
            ))

    def export_sources(self):
        esf = lambda x: os.path.join(self.export_sources_folder, x)
        rf =  lambda x: os.path.join(self.recipe_folder, x)
        copier = [
            # filter, src, dst, excludes
            ["*", rf("plugins"), esf("plugins"), None],
            ["*", rf("resources"), esf("resources"), "*.mo"],
            ["*", rf("tests"), esf("tests"), None],
            ["*", rf("cura"), esf("cura"), "BladeVersion.py"],
            ["*", rf("packaging"), esf("packaging"), None],
            ["*", rf(".run_templates"), esf(".run_templates"), None],
            ["requirements.txt", self.recipe_folder, self.export_sources_folder, None],
            ["requirements-dev.txt", self.recipe_folder, self.export_sources_folder, None],
            ["requirements-ultimaker.txt", self.recipe_folder, self.export_sources_folder, None],
            ["BladeVersion.py.jinja", self.recipe_folder, self.export_sources_folder, None],
            ["cura_app.py", self.recipe_folder, self.export_sources_folder, None],
        ]
        for filter, src, dst, exclude in copier:
            copy(self, filter, src=src, dst=dst, excludes=exclude)

    def configure(self):
        self.options["pyarcus"].shared = True
        self.options["pysavitar"].shared = True
        self.options["pynest2d"].shared = True
        self.options["python-for-blade"].shared = True

        if not self.in_local_cache:
            self._vscode_workspace()

    def _vscode_workspace(self):
        self.output.info("creating vscode workspace.")
        root_dir = Path(__file__).parent
        template_file = root_dir.joinpath(".run_templates","conan-generated-client.code-workspace.jinja")
        templater = Template(template_file.read_text())

        out_file = root_dir.joinpath("conan-generated-client.code-workspace")
        out_file.write_text(templater.render(
            date_time = str(datetime.datetime),
            project_path = root_dir.as_posix(),
        ))

    def requirements(self):
        for req in self.requirements_list():
            self.requires(req)

    def layout(self):
        self.folders.source = "."
        self.folders.build = "venv"
        self.folders.generators = os.path.join(self.folders.build, "conan")

        self.cpp.package.libdirs = [os.path.join("site-packages", "cura")]
        self.cpp.package.bindirs = ["bin"]
        self.cpp.package.resdirs = ["resources", "plugins", "packaging", "pip_requirements"]  # pip_requirements should be the last item in the list

    def build(self):
        if self.options.devtools:
            if self.settings.os != "Windows" or self.conf.get("tools.microsoft.bash:path", check_type = str):
                # FIXME: once m4, autoconf, automake are Conan V2 ready use self.win_bash and add gettext as base tool_requirement
                cpp_info = self.dependencies["gettext"].cpp_info
                for po_file in self.source_path.joinpath("resources", "i18n").glob("**/*.po"):
                    mo_file = self.build_path.joinpath(po_file.with_suffix('.mo').relative_to(self.source_path))
                    mkdir(self, str(unix_path(self, mo_file.parent)))
                    self.run(f"{cpp_info.bindirs[0]}/msgfmt {po_file} -o {mo_file} -f", env="conanbuild", ignore_errors=True)

        self.output.info("Creating final changelog from materials and client")
        self.run(f"python ..\\scripts\\process_changelog.py -c {self.channel}", run_environment=True)
        self._setup_blade_url()

    def _setup_blade_url(self):
        """Builds a file from template that specifies all Blade related URL files"""
        with open(os.path.join(self.recipe_folder,"cura","UltimakerCloud", "BigRepCloudConstants.py.jinja"), "r") as f:
            bigrep_url = Template(f.read())

        with open(os.path.join(self.recipe_folder,"cura","UltimakerCloud", "BigRepCloudConstants.py"), "w") as f:
            f.write(bigrep_url.render(
                name = str(self.options.display_name).replace(" ", "-"),
                bigrep_cloud_api_root = "\"https://idp.qa-serviceplatform.bigrep.com/auth/realms/bigrep/protocol/openid-connect\"" if self.options.internal else "\"https://idp.serviceplatform.bigrep.com/auth/realms/bigrep/protocol/openid-connect\"",
                bigrep_service_platform_api_root  = "\"https://qa-serviceplatform.bigrep.com\"" if self.options.internal else "\"https://serviceplatform.bigrep.com\"",
                bigrep_connect_api_root = "\"https://qa-connect.bigrep.com\"" if self.options.internal else "\"https://connect.bigrep.com\"",
                bigrep_connect_auth = "\"https://blade-auth.qa-connect.bigrep.com\""  if self.options.internal else "\"https://blade-auth.connect.bigrep.com\""
            ))

    def generate(self):
        copy(self, "cura_app.py", self.source_folder, str(self._script_dir))
        cura_run_envvars = self._cura_run_env.vars(self, scope = "run")
        ext = ".ps1" if self.settings.os == "Windows" else ".sh"
        cura_run_envvars.save_script(os.path.join(self.folders.generators, f"cura_run_environment{ext}"))

        vr = VirtualRunEnv(self)
        vr.generate()

        self._generate_version(Path(self.source_folder, "cura"))

        # if self.options.devtools:
        #     entitlements_file = "'{}'".format(Path(self.source_folder, "packaging", "MacOS", "cura.entitlements"))
        #     self._generate_pyinstaller_spec(location = self.generators_folder,
        #                                     entrypoint_location = "'{}'".format(Path(self.source_folder, self.conan_data["runinfo"]["entrypoint"])).replace("\\", "\\\\"),
        #                                     icon_path = "'{}'".format(Path(self.source_folder, "packaging", self.conan_data["pyinstaller"]["icon"][str(self.settings.os)])).replace("\\", "\\\\"),
        #                                     entitlements_file = entitlements_file if self.settings.os == "Macos" else "None")

        #     # Update the po files
        #     if self.settings.os != "Windows" or self.conf.get("tools.microsoft.bash:path", check_type = str):
        #         # FIXME: once m4, autoconf, automake are Conan V2 ready use self.win_bash and add gettext as base tool_requirement
        #         cpp_info = self.dependencies["gettext"].cpp_info
        #         for po_file in self.source_path.joinpath("resources", "i18n").glob("**/*.po"):
        #             pot_file = self.source_path.joinpath("resources", "i18n", po_file.with_suffix('.pot').name)
        #             mkdir(self, str(unix_path(self, pot_file.parent)))
        #             self.run(f"{cpp_info.bindirs[0]}/msgmerge --no-wrap --no-fuzzy-matching -width=140 -o {po_file} {po_file} {pot_file}",
        #                      env = "conanbuild", ignore_errors = True)

    def imports(self):
        self.copy("BladeEngine.exe", root_package = "engine", src = "@bindirs", dst = "", keep_path = False)
        self.copy("BladeEngine", root_package = "engine", src = "@bindirs", dst = "", keep_path = False)

        # Blade specific import of custom packages
        package_list = ["third-party-plugins", "blade-materials", "auto-orientation", "post-processing", "data-collector"]
        for package in package_list:
            for resource_dir in self.deps_cpp_info[package].resdirs:
                self.copy("*", root_package=package, src=resource_dir, dst=resource_dir, keep_path=True)
                self.output.info(f"Copied: {resource_dir} from package: {package}")
            self.output.info("----------------------------")
        # Copy resources of cura_binary_data, only one folder
        package = "blade-binary-data"
        self.copy("*", root_package = package, src = self.deps_cpp_info[package].resdirs[0],keep_path = True)
        self.output.info(f"Copied: {self.deps_cpp_info[package].resdirs[0]} from {package}")
        self.output.info("----------------------------")
        self.copy("icons/blade.ico", root_package = package, src = self.deps_cpp_info[package].resdirs[0], dst="packaging/icons", keep_path = False)
        self.output.info(f"Copied: icons/blade.ico from {self.deps_cpp_info[package].resdirs[0]}")

        self.output.info(self._site_packages)
        self.copy("*.dll", src = "@bindirs", dst = self._site_packages)
        self.copy("*.pyd", src = "@libdirs", dst = self._site_packages)
        self.copy("*.pyi", src = "@libdirs", dst = self._site_packages)
        self.copy("*.dylib", src = "@libdirs", dst = self._script_dir)

    def deploy(self):
        # Copy BladeEngine.exe to bindirs of Virtual Python Environment
        # TODO: Fix source such that it will get the BladeEngine relative from the executable (Python bindir in this case)
        self.copy_deps("BladeEngine.exe", root_package = "engine", src = self.deps_cpp_info["engine"].bindirs[0],dst = self._base_dir,keep_path = False)
        self.copy_deps("BladeEngine", root_package = "engine", src = self.deps_cpp_info["engine"].bindirs[0], dst = self._base_dir,keep_path = False)

        # Copy resources of Cura (keep folder structure)
        self.copy("*", src = self.cpp_info.bindirs[0], dst = self._base_dir, keep_path = False)
        self.copy("*", src = self.cpp_info.libdirs[0], dst = self._site_packages.joinpath("cura"), keep_path = True)
        self.copy("*", src = self.cpp_info.resdirs[0], dst = self._share_dir.joinpath("cura", "resources"), keep_path = True)
        self.copy("*", src = self.cpp_info.resdirs[1], dst = self._share_dir.joinpath("cura", "plugins"), keep_path = True)

        # Copy materials (flat) From cura method
        # self.copy_deps("*.fdm_material", root_package = "blade-materials", src = self.deps_cpp_info["blade-materials"].resdirs[0],
        #                dst = self._share_dir.joinpath("cura", "resources", "materials"), keep_path = False)
        # self.copy_deps("*.sig", root_package = "blade-materials", src = self.deps_cpp_info["blade-materials"].resdirs[0],
        #                dst = self._share_dir.joinpath("cura", "resources", "materials"), keep_path = False)

        # Copy internal resources
        # if self.options.internal:
        #     self.copy_deps("*", root_package = "cura_private_data", src = self.deps_cpp_info["cura_private_data"].resdirs[0],
        #                    dst = self._share_dir.joinpath("cura", "resources"), keep_path = True)
        #     self.copy_deps("*", root_package = "cura_private_data", src = self.deps_cpp_info["cura_private_data"].resdirs[1],
        #                    dst = self._share_dir.joinpath("cura", "plugins"), keep_path = True)

        # Copy resources of Uranium (keep folder structure)
        self.copy_deps("*", root_package = "uranium", src = self.deps_cpp_info["uranium"].resdirs[0],
                       dst = self._share_dir.joinpath("uranium", "resources"), keep_path = True)
        self.copy_deps("*", root_package = "uranium", src = self.deps_cpp_info["uranium"].resdirs[1],
                       dst = self._share_dir.joinpath("uranium", "plugins"), keep_path = True)
        self.copy_deps("*", root_package = "uranium", src = self.deps_cpp_info["uranium"].libdirs[0],
                       dst = self._site_packages.joinpath("UM"),
                       keep_path = True)
        self.copy_deps("*", root_package = "uranium", src = str(os.path.join(self.deps_cpp_info["uranium"].libdirs[0], "Qt", "qml", "UM")),
                       dst = self._site_packages.joinpath("PyQt6", "Qt6", "qml", "UM"),
                       keep_path = True)

        # Copy resources of blade-binary-data
        self.copy_deps("*", root_package = "blade-binary-data", src = self.deps_cpp_info["blade-binary-data"].resdirs[0],
                       dst = self._share_dir.joinpath("cura"), keep_path = True)
        self.copy_deps("*", root_package = "blade-binary-data", src = self.deps_cpp_info["blade-binary-data"].resdirs[1],
                       dst = self._share_dir.joinpath("uranium"), keep_path = True)

        self.copy_deps("*.dll", src = "@bindirs", dst = self._site_packages)
        self.copy_deps("*.pyd", src = "@libdirs", dst = self._site_packages)
        self.copy_deps("*.pyi", src = "@libdirs", dst = self._site_packages)
        self.copy_deps("*.lib", src = "@libdirs", dst = self._site_packages)
        self.copy_deps("*.dylib", src = "@libdirs", dst = self._base_dir.joinpath("lib"))

        # Copy packaging scripts
        self.copy("*", src = self.cpp_info.resdirs[2], dst = self._base_dir.joinpath("packaging"))

        # Copy requirements.txt's
        self.copy("*.txt", src = self.cpp_info.resdirs[-1], dst = self._base_dir.joinpath("pip_requirements"))

        # Generate the Gitlab version info Environment
        # version = self.conf_info.get("user.cura:version", default = self.version, check_type = str)
        blade_version = Version(self.version)

        # this is used by create_windows_installer.py
        env_prefix = "Env:" if self.settings.os == "Windows" else ""
        activate_gitlab_envs = Template(r"""${{ env_prefix }}BLADE_VERSION_MAJOR="{{ blade_version_major }}"
${{ env_prefix }}BLADE_VERSION_MINOR="{{ blade_version_minor }}"
${{ env_prefix }}BLADE_VERSION_PATCH="{{ blade_version_patch }}"
${{ env_prefix }}BLADE_VERSION_FULL="{{ blade_version_full }}"
${{ env_prefix }}BLADE_APP_NAME="{{ blade_app_name }}"
        """).render(blade_version_major = blade_version.major,
                    blade_version_minor = blade_version.minor,
                    blade_version_patch = blade_version.patch, # NSIS needs VIProductVersion format, should be X.X.X, not X.X.X-feature-777
                    blade_version_build = blade_version.micro if blade_version.micro != "" else "0",
                    blade_version_full = self._full_verison_str,
                    blade_app_name = f"{self._app_name} {self._full_verison_str}",
                    env_prefix = env_prefix)

        ext = ".sh" if self.settings.os != "Windows" else ".ps1"
        save(self, os.path.join(self._script_dir, f"activate_gitlab_envs{ext}"), activate_gitlab_envs)

        # self._generate_version(os.path.join(self._site_packages, "cura"))
        entitlements_file = "'{}'".format(Path(self.cpp_info.res_paths[2], "MacOS", "cura.entitlements")) if self.settings.os == "Macos" else "None"

        v = f"{blade_version.major}.{blade_version.minor}.{blade_version.patch}"
        if v not in self.conan_data["requirements"] and not self.options.internal:
            raise ValueError(f"Attempting to deploy an external version = {self.version}. However external versions must be frozen")

        if v not in self.conan_data:
            self.output.warning(f"Version = {self.version} not found in `conandata.yml` using the `any` category.")
            v = "any"

        self._generate_pyinstaller_spec(
            self._base_dir,
            self.conan_data["pyinstaller"],
            entitlements_file,
        )

    def package(self):
        copy(self, "cura_app.py", src = self.source_folder, dst = os.path.join(self.package_folder, self.cpp.package.bindirs[0]))
        if self.options.internal:
            copy(self, "clean_blade_appdata.ps1", src = os.path.join(self.source_folder,"scripts"), dst = os.path.join(self.package_folder, self.cpp.package.bindirs[0]))

        copy(self, "*", src = os.path.join(self.source_folder, "cura"), dst = os.path.join(self.package_folder, self.cpp.package.libdirs[0]))
        copy(self, "*", src = os.path.join(self.source_folder, "resources"), dst = os.path.join(self.package_folder, self.cpp.package.resdirs[0]))
        copy(self, "*.mo", os.path.join(self.build_folder, "resources"), os.path.join(self.package_folder, "resources"))
        copy(self, "*", src = os.path.join(self.source_folder, "plugins"), dst = os.path.join(self.package_folder, self.cpp.package.resdirs[1]))
        copy(self, "requirement*.txt", src = self.source_folder, dst = os.path.join(self.package_folder, self.cpp.package.resdirs[-1]))
        copy(self, "*", src = os.path.join(self.source_folder, "packaging"), dst = os.path.join(self.package_folder, self.cpp.package.resdirs[2]))

    def package_info(self):
        self.user_info.pip_requirements = "requirements.txt"
        self.user_info.pip_requirements_git = "requirements-ultimaker.txt"
        self.user_info.pip_requirements_build = "requirements-dev.txt"

        if self.in_local_cache:
            self.runenv_info.append_path("PYTHONPATH", os.path.join(self.package_folder, "site-packages"))
            self.runenv_info.append_path("PYTHONPATH", os.path.join(self.package_folder, "plugins"))
        else:
            self.runenv_info.append_path("PYTHONPATH", self.source_folder)
            self.runenv_info.append_path("PYTHONPATH", os.path.join(self.source_folder, "plugins"))

    def package_id(self):
        self.info.clear()

        # The following options shouldn't be used t
        
        # this is used by create_windows_installer.pyo determine the hash, since these are only used to set the CuraVersion.py
        # which will als be generated by the deploy method during the `conan install cura/5.1.0@_/_`
        del self.info.options.enterprise
        del self.info.options.staging
        del self.info.options.devtools
        # del self.info.options.cloud_api_version
        # del self.info.options.cloud_api_version
        del self.info.options.display_name
        del self.info.options.cura_debug_mode

        # TODO: Use the hash of requirements.txt and requirements-ultimaker.txt, Because changing these will actually result in a different
        #  Cura. This is needed because the requirements.txt aren't managed by Conan and therefor not resolved in the package_id. This isn't
        #  ideal but an acceptable solution for now.
