"""Manipulates installer files"""

import os
from gettext import gettext as _
from typing import Optional
from urllib.parse import urlparse

from lutris.cache import get_cache_path, has_custom_cache_path, save_to_cache
from lutris.gui.widgets.download_progress_box import DownloadProgressBox
from lutris.installer.errors import ScriptingError
from lutris.util import system
from lutris.util.downloader import Downloader
from lutris.util.log import logger
from lutris.util.strings import gtk_safe_urls


class InstallerFile:
    """Representation of a file in the `files` sections of an installer"""

    def __init__(self, game_slug, file_id, file_meta):
        self.game_slug = game_slug
        self.id = file_id.replace("-", "_")  # pylint: disable=invalid-name
        self._file_meta = file_meta
        self._dest_file_override = None  # Used to override the destination
        self._dest_file_found = None  # Lazy storage for the resolved destination file
        if isinstance(self._file_meta, dict):
            self._downloader = self._file_meta.get("downloader")
        else:
            self._downloader = None

    def copy(self):
        """Copies this file object, so the copy can be modified safely."""
        _file_meta = self._file_meta
        if isinstance(_file_meta, dict):
            _file_meta = _file_meta.copy()

        file = InstallerFile(self.game_slug, self.id, _file_meta)
        file._dest_file_override = self._dest_file_override
        file._dest_file_found = self._dest_file_found
        file._downloader = self._downloader
        return file

    @property
    def url(self):
        _url = ""
        if isinstance(self._file_meta, dict):
            if "url" not in self._file_meta:
                raise ScriptingError(_("missing field `url` for file `%s`") % self.id)
            _url = self._file_meta["url"]
        else:
            _url = self._file_meta
        if _url.startswith("/"):
            return "file://" + _url
        return _url

    def set_url(self, url):
        """Change the internal value of the URL"""
        if isinstance(self._file_meta, dict):
            self._file_meta["url"] = url
        else:
            self._file_meta = url

    @property
    def filename(self):
        if isinstance(self._file_meta, dict):
            if "filename" not in self._file_meta:
                raise ScriptingError(_("missing field `filename` in file `%s`") % self.id)
            return self._file_meta["filename"]
        if self._file_meta.startswith("N/A"):
            if self.uses_pga_cache() and os.path.isdir(self.cache_path):
                return self.cached_filename
            return ""
        if self.url.startswith("$STEAM"):
            return self.url
        return os.path.basename(self._file_meta)

    def get_alternate_filenames(self):
        if isinstance(self._file_meta, dict):
            return self._file_meta.get("alternate_filenames") or []
        return []

    @property
    def referer(self):
        if isinstance(self._file_meta, dict):
            return self._file_meta.get("referer")

    @property
    def downloader(self) -> Optional[Downloader]:
        if callable(self._downloader):
            self._downloader = self._downloader(self)

        return self._downloader

    @property
    def checksum(self):
        if isinstance(self._file_meta, dict):
            return self._file_meta.get("checksum")

    @property
    def dest_file(self):
        def find_dest_file():
            for alt_name in self.get_alternate_filenames():
                alt_path = os.path.join(self.cache_path, alt_name)
                if os.path.isfile(alt_path):
                    return alt_path

            return os.path.join(self.cache_path, self.filename)

        if self._dest_file_override:
            return self._dest_file_override

        if not self._dest_file_found:
            self._dest_file_found = find_dest_file()

        return self._dest_file_found

    @dest_file.setter
    def dest_file(self, value):
        self._dest_file_override = value

    @property
    def download_file(self):
        """This is the actual path to download to; this file is renamed to the
        dest_file when complete."""
        return self.dest_file + ".tmp"

    def override_dest_file(self, new_dest_file):
        """Called by the UI when the user selects a file path."""
        self.dest_file = new_dest_file

    @property
    def is_dest_file_overridden(self):
        return bool(self._dest_file_override)

    def get_dest_files_by_id(self):
        return {self.id: self.dest_file}

    def __str__(self):
        return "%s/%s" % (self.game_slug, self.id)

    @property
    def auxiliary_info(self):
        """Provides a small bit of additional descriptive texts to show in the UI."""
        return None

    @property
    def human_url(self):
        """Return the url in human-readable format"""
        if self.url.startswith("N/A"):
            # Ask the user where the file is located
            parts = self.url.split(":", 1)
            if len(parts) == 2:
                return parts[1]
            return "Please select file '%s'" % self.id
        return self.url

    def get_label(self):
        """Return a human readable label for installer files"""
        url = self.url
        if url.startswith("http"):
            parsed = urlparse(url)
            label = _("{file} on {host}").format(file=self.filename, host=parsed.netloc)
        elif url.startswith("N/A"):
            label = url[3:].lstrip(":")
        else:
            label = url
        return gtk_safe_urls(label)

    @property
    def cached_filename(self):
        """Return the filename of the first file in the cache path"""
        cache_files = os.listdir(self.cache_path)
        if cache_files:
            return cache_files[0]
        return ""

    @property
    def default_provider(self):
        """Return file provider used"""
        if self.url.startswith("$STEAM"):
            return "steam"
        if self.is_cached:
            return "pga"
        if self.url.startswith("N/A"):
            return "user"
        if self.is_downloadable():
            return "download"
        raise ValueError("Unsupported provider for %s" % self.url)

    @property
    def providers(self):
        """Return all supported providers"""
        _providers = set()
        if self.url.startswith("$STEAM"):
            _providers.add("steam")
        if self.is_cached:
            _providers.add("pga")
        if self.url.startswith("N/A"):
            _providers.add("user")
        if self.is_downloadable():
            _providers.add("download")
        return _providers

    def is_downloadable(self):
        """Return True if the file can be downloaded (even from the local filesystem)"""
        return self.url.startswith(("http", "file"))

    def uses_pga_cache(self):
        """Determines whether the installer files are stored in a PGA cache

        Returns:
            bool
        """
        if self.url.startswith("N/A"):
            return False
        return has_custom_cache_path()

    @property
    def is_user_pga_caching_allowed(self):
        """Returns true if this file can be transferred to the cache, if
        the user provides it."""
        return self.uses_pga_cache()

    @property
    def cache_path(self):
        """Return the directory used as a cache for the duration of the installation"""
        _cache_path = get_cache_path()
        url_parts = urlparse(self.url)
        if url_parts.netloc.endswith("gog.com"):
            folder = "gog"
        else:
            folder = self.id
        return os.path.join(_cache_path, self.game_slug, folder)

    def prepare(self):
        """Prepare the file for download. If we've not been redirected to an existing file,
        and if we're using our own installer cache, we need to unsure that directory exists."""
        if not self.is_dest_file_overridden and not system.path_exists(self.cache_path):
            os.makedirs(self.cache_path)

    def create_download_progress_box(self):
        return DownloadProgressBox(
            url=self.url, dest=self.dest_file, temp=self.download_file, referer=self.referer, downloader=self.downloader
        )

    def check_hash(self):
        """Checks the checksum of `file` and compare it to `value`

        Args:
            checksum (str): The checksum to look for (type:hash)
            dest_file (str): The path to the destination file
            dest_file_uri (str): The uri for the destination file
        """
        if not self.checksum or not self.dest_file:
            return
        try:
            hash_type, expected_hash = self.checksum.split(":", 1)
        except ValueError as err:
            raise ScriptingError(_("Invalid checksum, expected format (type:hash) "), self.checksum) from err

        logger.info("Checking hash %s for %s", hash_type, self.dest_file)
        calculated_hash = system.get_file_checksum(self.dest_file, hash_type)
        if calculated_hash != expected_hash:
            raise ScriptingError(
                hash_type.capitalize() + _(" checksum mismatch "), f"{expected_hash} != {calculated_hash}"
            )

    @property
    def size(self) -> Optional[int]:
        if isinstance(self._file_meta, dict) and "size" in self._file_meta:
            try:
                size = int(self._file_meta["size"])
                if size >= 0:
                    return size
            except (ValueError, TypeError):
                return None
        return None

    @property
    def total_size(self) -> Optional[int]:
        if isinstance(self._file_meta, dict) and "total_size" in self._file_meta:
            try:
                total_size = int(self._file_meta["total_size"])
                if total_size >= 0:
                    return total_size
            except (ValueError, TypeError):
                return None
        return None

    def is_ready(self, provider):
        """Is the file already present at the destination (if applicable)?"""
        return provider not in ("user", "pga") or system.path_exists(self.dest_file)

    @property
    def is_cached(self):
        """Is the file available in the local PGA cache?"""
        return self.uses_pga_cache() and system.path_exists(self.dest_file)

    def save_to_cache(self):
        """Copy the file into the PGA cache."""

        cache_path = self.cache_path
        try:
            if not os.path.isdir(cache_path):
                logger.debug("Creating cache path %s", self.cache_path)
                os.makedirs(cache_path)
        except (OSError, PermissionError) as ex:
            logger.error("Failed to created cache path: %s", ex)
            return

        save_to_cache(self.dest_file, cache_path)

    def remove_previous(self):
        """Remove file at already at destination, prior to starting the download."""
        if not self.uses_pga_cache() and system.path_exists(self.dest_file):
            # If we've previously downloaded a directory, we'll need to get rid of it
            # to download a file now. Since we are not using the cache, we don't keep
            # these files anyway - so it should be safe to just nuke and pave all this.
            if os.path.isdir(self.dest_file):
                system.delete_folder(self.dest_file)
            else:
                os.remove(self.dest_file)
