# Copyright 2017, 2018 Amazon.com, Inc. or its affiliates.

# This module is part of Amazon Linux Extras.
#
# Amazon Linux Extras is free software: you can redistribute it and/or
# modify it under the terms of the GNU General Public License v2 as published
# by the Free Software Foundation.
#
# Amazon Linux Extras 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 General Public License
# for more details.
#
# You should have received a copy of the GNU General Public License
# along with Amazon Linux Extras.  If not, see <http://www.gnu.org/licenses/>.

from __future__ import print_function, unicode_literals

import ast
import os
import sys
import json
from time import time
from tempfile import gettempdir, NamedTemporaryFile
import logging as loggingmod
import subprocess

if sys.version_info.major == 2:
    from gettext import gettext as gettext_yields_encoded
    _ = lambda *args: gettext_yields_encoded(*args).decode("UTF-8")
else:
    from gettext import gettext as _

try:
    from urllib2 import urlopen
    from urllib2 import URLError
except ImportError:
    from urllib.request import urlopen
    from urllib.error import URLError


CATALOG_URL = os.environ.get("CATALOGURL", "http://amazonlinux.{awsregion}.{awsdomain}/{releasever}/extras-catalog.json")

IS_TESTING = os.environ.get("AMZN2DEBUGGING", False) and True

ACTUAL_UID = int(os.environ.get("SUDO_UID", os.getuid()))
CACHE_FILE_NAME = os.path.join(gettempdir(), "amzn2extras-{0}".format(ACTUAL_UID))

VERSION_KEY = "_exact_version_"

logger = loggingmod.getLogger(__name__)


class CatalogError(ValueError): """List of software is not usable."""


def make_catalog_lookups_easy(catalog):
    # a fast look-up by topic name to get topic data.
    catalog["topics-by-name"] = dict((topic["n"], topic) for topic in catalog.get("topics", []))
    # expand topic indexes into sets of names, for easy comparison.
    catalog["whitelists-by-names"] = [list(catalog["topics"][i]["n"] for i in wli) for wli in catalog["whitelists"]]
    return catalog


def get_yum_variables():
    """Query Yum subsystem for its variable storage, and then override those
    values when they're in environment."""

    discovered_vars = None
    yumvars = {}
    for name, default in (("awsdomain", "amazonaws.com"), ("awsregion", "default"), ("releasever", "2")):
        override_value = os.environ.get(name.upper())

        if override_value is None and discovered_vars is None:
            # 'yum' is py2 only, and we might be py3, so we can't just import.
            yum_process = subprocess.Popen(["env", "-i", "python", "-c", "import yum; y=yum.YumBase(); y.doConfigSetup(init_plugins=False); print(y.conf.yumvar)"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            yum_output, _ = yum_process.communicate()
            try:
                discovered_vars = ast.literal_eval(yum_output.decode("UTF-8"))
            except Exception as exc:
                logger.warning("Querying yum for its variables failed.")
                discovered_vars = {}

        yumvars[name] = override_value or discovered_vars.get(name) or default

    return yumvars


def fetch_new_catalog():
    """Get a data structure that represents what we know of available
    software."""

    yumvars = get_yum_variables()
    url = CATALOG_URL.format(**yumvars)
    try:
        request = urlopen(url)
        fetched_string = request.read().decode("UTF-8")
    except URLError:
        logger.error(_("Catalog is not reachable. Try again later."))
        logger.exception("catalog at %s", url)
        raise CatalogError("not reachable")
    catalog = json.loads(fetched_string)
    make_catalog_lookups_easy(catalog)

    return catalog


def get_catalog(insist_stable_ordinal=False):
    """Load the Bonus-software catalog from disk or network and check its sanity.

    Some user commands can take numbers as arguments, and when that happens and
    that command uses this fn to decide what the user meant, then we must refuse
    to renumber the items that the user saw. `insist_stable_ordinal` means we do
    not load something new, but what the user last saw.
    """
    catalog = load_cached_catalog()
    if not catalog or IS_TESTING:
        if insist_stable_ordinal and not IS_TESTING:
            logger.error(_("The catalog of Extras might have changed. Please \"list\" to refresh."))
            raise CatalogError("perhaps not consistent numbering")

        catalog = fetch_new_catalog()

    if catalog.get("motd"):
        print(catalog["motd"], file=sys.stderr)  # translations? :(

        if catalog.get("status", "ok") != "ok":
            # Maybe this is disabled.
            if not IS_TESTING:
                sys.exit(1)

    if catalog.get("version") != 1:
        logger.error(_("The catalog version is newer than this tool understands. Please upgrade."))
        logger.error("$ sudo yum upgrade amazon-linux-extras")
        raise CatalogError("catalog is too new")

    try:
        store_catalog_in_cache(catalog)
    except CatalogError:
        raise
    except Exception:
        logger.info(_("Couldn't write catalog to cache."))
    return catalog


def load_cached_catalog():
    """Get a catalog off of disk. Return it if it is safe, but return False if something about it
    is wrong."""
    if os.path.islink(CACHE_FILE_NAME):
        return False

    if not os.access(CACHE_FILE_NAME, os.W_OK):
        # Unwritable caches are not trustable.
        return False

    if os.path.getmtime(CACHE_FILE_NAME) < (time()-(5*60)):  # five minutes.
        return False

    with open(CACHE_FILE_NAME) as cache_file:
        try:
            catalog = json.load(cache_file)
        except ValueError:
            return False

    if "version" not in catalog: return False
    if "topics" not in catalog: return False
    if "whitelists" not in catalog: return False
    if not catalog["topics"]: return False

    return catalog


def store_catalog_in_cache(catalog):
    """Preserve the catalog for immediate use again. Keep it writable by the
    real user, not an escalated role."""
    if not catalog:
        logger.debug("There is no catalog.")
        return

    with NamedTemporaryFile(mode="w+t", delete=False) as ntf:
        written_file_name = ntf.name
        try:
            json.dump(catalog, ntf)
        except Exception:
            raise CatalogError("can't store")  # Remap because we only care about this one.

    if os.geteuid() != ACTUAL_UID:
        logger.debug("Not running as normal user.")
        os.chown(written_file_name, ACTUAL_UID, -1)  # change ownership back to original user

    logger.debug("writing cache to %s", CACHE_FILE_NAME)
    os.rename(ntf.name, CACHE_FILE_NAME)


