# Source code of the Ubuntu-it website
# Copyright (C) 2016 Pietro Albini <pietroalbini@ubuntu.com>
# Copyright (C) 2023 shadMod <m.allegro@shadmod.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import hashlib
import json
import toml
import os
import random
import collections

import flask
import requests
import itsdangerous

from ... import cache
from ... import launchpad

DOWNLOAD_FILE = "downloads.toml"
CACHE_FILE = "download-cache.json"
CACHE_FILE_VERSION = 1


class Downloads:

    def __init__(self, data_path, debug: bool = False):
        # init _mirrors and _sha256sums
        self._mirrors = None
        self._sha256sums = None

        # Load the configuration
        path = os.path.join(data_path, DOWNLOAD_FILE)
        self.config = toml.load(
            path, _dict=collections.OrderedDict,
        )

        # Save the hash of the configuration
        with open(path, "rb") as raw:
            self._config_hash = "sha1=%s" % hashlib.sha1(raw.read()).hexdigest()

        if debug is False:
            self._cache_file = os.path.join(data_path, CACHE_FILE)

    @property
    def _cache(self):  # Cacheception
        """Get the cache from the data directory"""
        if not hasattr(self, "_cache_cache"):  # Naming things is hard
            self._cache_cache = self._cache_content()
        return self._cache_cache

    @property
    def mirrors(self):
        """Get a list of CD mirrors needed by the website"""
        if not hasattr(self, "_mirrors") or self._mirrors is None:
            if self._cache is not None:
                self._mirrors = self._cache["mirrors"]
            else:
                self._mirrors = {}
                for distro in self.config["mirrors"]["for"]:
                    found_mirrors = list(sorted(launchpad.get_cdimage_mirrors(
                        distro, self.config["mirrors"]["country"]
                    )))
                    if "it1.mirror.vhosting-it.com" in found_mirrors[0]:
                        found_mirrors = ["https://releases.ubuntu.com/"]
                    if found_mirrors:
                        self._mirrors[distro] = found_mirrors
                if not self._mirrors:
                    raise ValueError(
                        "No available mirrors found for %(for)s in %(country)s"
                        % self.config["mirrors"]
                    )
        return self._mirrors

    @property
    def sha256sums(self):
        """Get a list of all the SHA256SUMS"""
        if getattr(self, "_sha256sums") is None:
            if self._cache is not None:
                self._sha256sums = self._cache["sha256sums"]
            else:
                self._sha256sums = self._fetch_sha256sums()
        return self._sha256sums

    def _fetch_sha256sums(self):
        """Fetch all the needed SHA256SUMS"""
        result = {}
        files_content = {}

        for distro, config in self.config["distros"].items():
            for release in config["releases"]:
                for arch in config["archs"].keys():
                    url = self.url_for(distro, release, arch, use_random=False)
                    path, file = url.rsplit("/", 1)

                    # Fetch the file from the remote if it wasn't done already
                    if path not in files_content:
                        sha_hash = {}

                        response = requests.get("%s/SHA256SUMS" % path)
                        if response.status_code == 404:
                            raise RuntimeError(
                                "Missing {} {} on {}".format(distro, release, url),
                            )

                        for line in response.text.split("\n"):
                            if line.strip() == "":
                                continue

                            _hash, name = line.split(" ", 1)
                            if name.startswith("*"):
                                name = name[1:]
                            sha_hash[name] = _hash
                        files_content[path] = sha_hash

                    # Add the SHA256 to the result if it was found
                    if file in files_content[path]:
                        key = "%s:%s:%s" % (distro, release, arch)
                        result[key] = files_content[path][file]
                    else:
                        print(files_content[path])
                        raise RuntimeError("Missing SHA256SUMS for {}".format(file))

        return result

    def _cache_content(self):
        """Get the content of the cache in the data directory"""
        # The cache is returned if the file exists, the download configuration
        # didn't change, the file is valid JSON and the version is correct
        if not os.path.exists(self._cache_file):
            return None
        try:
            with open(self._cache_file) as f:
                content = json.load(
                    f,
                    object_pairs_hook=collections.OrderedDict,
                )
        except ValueError:
            return None
        if content["config-hash"] != self._config_hash:
            return None
        if content["version"] != CACHE_FILE_VERSION:
            return None
        return content["content"]

    def store_cache_file(self):
        """Store the cache file in the data directory"""
        # Store only if the cache isn't available
        if self._cache is None:
            with open(self._cache_file, "w") as f:
                json.dump({
                    "config-hash": self._config_hash,
                    "version": CACHE_FILE_VERSION,
                    "content": {
                        "mirrors": self.mirrors,
                        "sha256sums": self.sha256sums,
                    },
                }, f, separators=(",", ":"))

            # Invalidate the cache in this instance
            delattr(self, "_cache_cache")

    def url_for(self, distro, release, arch, torrent=False, use_random=True):
        """Get the URL of a download file"""
        # Build the dict of keys to replace
        data_rel = self.config["distros"][distro]["archs"][arch][release]
        replaces = {
            "distro": distro,
            "release": release,
            "arch": arch,
            "version": data_rel["version"],
            "codename": data_rel["codename"],
        }

        # Add mirrors to the replaces key
        for name, choices in self.mirrors.items():
            if use_random:
                replaces["mirror:%s" % name] = random.choice(choices)
            else:
                replaces["mirror:%s" % name] = choices[0]

        # Get the right source
        host = data_rel["host"]
        source = self.config["sources"][host]

        # Download URLs are different for torrent and HTTP
        if torrent:
            url = source["torrent"]
        else:
            url = source["https"]

        for key, value in replaces.items():
            url = url.replace("{%s}" % key, value)

        return url

    def prepare_blueprint(self, app):
        """Prepare a blueprint with the download pages"""
        signer = itsdangerous.URLSafeSerializer(app.secret_key)

        bp = flask.Blueprint("download", __name__)

        @bp.route("/")
        @cache.enable
        def index():
            return flask.render_template(
                "download/index.html",
                distros=self.config["distros"],
            )

        @bp.route("/<distro>")
        @cache.enable
        def landing(distro):
            if distro not in self.config["distros"]:
                flask.abort(404)

            data = self.config["distros"][distro]
            return flask.render_template(
                "download/landing.html",
                distro_name=distro,
                distro=data,
                releases=data["archs"]["amd64"],
                archs=self.config["archs"],
            )

        @bp.route("/+redirect", methods=["POST"])
        def redirect():
            # Return a Bad Request if not all the fields are present
            try:
                distro = flask.request.form["distro"]
                release = flask.request.form["release"]
                arch = flask.request.form["arch"]
            except KeyError:
                flask.abort(400)
                raise KeyError
            except Exception:
                raise Exception

                # Validate the input
            if distro not in self.config["distros"]:
                flask.abort(400)
            if release not in self.config["distros"][distro]["releases"]:
                flask.abort(400)
            if arch not in self.config["distros"][distro]["archs"]:
                flask.abort(400)

            torrent = "torrent" in flask.request.form

            # release = 'latest' if release == 'interim' else 'lts'
            data_rel = self.config["distros"][distro]["archs"][arch][release]
            payload = signer.dumps(
                {
                    "url": self.url_for(distro, release, arch, torrent),
                    "sha256": self.sha256sums[f"{distro}:{release}:{arch}"],
                    "name": "%s %s%s" % (
                        self.config["distros"][distro]["info"]["name"],
                        data_rel["version"],
                        " LTS" if data_rel["lts"] else "",
                    ),
                    "theme": distro,
                }
            )
            return flask.redirect(flask.url_for(".thanks", payload=payload))

        @bp.route("/grazie/<payload>")
        def thanks(payload):
            try:
                content = signer.loads(payload)
            except itsdangerous.BadSignature:
                return flask.redirect(flask.url_for("pages.download_index"))

            return flask.render_template("download/thanks.html", **content)

        return bp

    def generate_navbar(self):
        """Generate the sub-navbar for /download"""
        result = []
        for name, distro in self.config["distros"].items():
            result.append(
                {
                    "name": distro["info"]["name"],
                    "endpoint": "download.landing",
                    "endpoint-args": {"distro": name},
                }
            )
        return result
