# Microsoft Azure Linux Agent
#
# Copyright 2020 Microsoft Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Requires Python 2.6+ and Openssl 1.0+

import glob
import os
import shutil

from azurelinuxagent.common import conf, logger
from azurelinuxagent.common.exception import AgentUpdateError
from azurelinuxagent.common.future import ustr
from azurelinuxagent.common.protocol.extensions_goal_state import GoalStateSource
from azurelinuxagent.common.utils import fileutil
from azurelinuxagent.common.utils.flexible_version import FlexibleVersion
from azurelinuxagent.common.version import AGENT_NAME, AGENT_DIR_PATTERN, CURRENT_VERSION
from azurelinuxagent.ga.guestagent import GuestAgent, AGENT_MANIFEST_FILE


class GAVersionUpdater(object):

    def __init__(self, gs_id):
        self._gs_id = gs_id
        self._version = FlexibleVersion("0.0.0.0")  # Initialize to zero and retrieve from goal state later stage
        self._agent_manifest = None  # Initialize to None and fetch from goal state at different stage for different updater

    def is_update_allowed_this_time(self, ext_gs_updated):
        """
        This function checks if we allowed to update the agent.
        @param ext_gs_updated: True if extension goal state updated else False
        @return false when we don't allow updates.
        """
        raise NotImplementedError

    def is_rsm_update_enabled(self, agent_family, ext_gs_updated):
        """
        return True if we need to switch to RSM-update from self-update and vice versa.
        @param agent_family: agent family
        @param ext_gs_updated: True if extension goal state updated else False
        @return: False when agent need to stop rsm updates
                 True: when agent need to switch to rsm update
        """
        raise NotImplementedError

    def retrieve_agent_version(self, agent_family, goal_state):
        """
        This function fetches the agent version from the goal state for the given family.
        @param agent_family: agent family
        @param goal_state: goal state
        """
        raise NotImplementedError

    def is_retrieved_version_allowed_to_update(self, agent_family):
        """
        Checks all base condition if new version allow to update.
        @param agent_family: agent family
        @return: True if allowed to update else False
        """
        raise NotImplementedError

    def log_new_agent_update_message(self):
        """
        This function logs the update message after we check agent allowed to update.
        """
        raise NotImplementedError

    def proceed_with_update(self):
        """
        performs upgrade/downgrade
        @return: AgentUpgradeExitException
        """
        raise NotImplementedError

    @property
    def version(self):
        """
        Return version
        """
        return self._version

    def sync_new_gs_id(self, gs_id):
        """
        Update gs_id
        @param gs_id: goal state id
        """
        self._gs_id = gs_id

    @staticmethod
    def download_new_agent_pkg(package_to_download, protocol, is_fast_track_goal_state):
        """
        Function downloads the new agent.
        @param package_to_download: package to download
        @param protocol: protocol object
        @param is_fast_track_goal_state: True if goal state is fast track else False
        """
        agent_name = "{0}-{1}".format(AGENT_NAME, package_to_download.version)
        agent_dir = os.path.join(conf.get_lib_dir(), agent_name)
        agent_pkg_path = ".".join((os.path.join(conf.get_lib_dir(), agent_name), "zip"))
        agent_handler_manifest_file = os.path.join(agent_dir, AGENT_MANIFEST_FILE)
        if not os.path.exists(agent_dir) or not os.path.isfile(agent_handler_manifest_file):
            protocol.client.download_zip_package(agent_name, package_to_download.uris, agent_pkg_path, agent_dir, use_verify_header=is_fast_track_goal_state,
                                                 signature="", enforce_signature=False)
        else:
            logger.info("Agent {0} was previously downloaded - skipping download", agent_name)

        if not os.path.isfile(agent_handler_manifest_file):
            try:
                # Clean up the agent directory if the manifest file is missing
                logger.info("Agent handler manifest file is missing, cleaning up the agent directory: {0}".format(agent_dir))
                if os.path.isdir(agent_dir):
                    shutil.rmtree(agent_dir, ignore_errors=True)
            except Exception as err:
                logger.warn("Unable to delete Agent directory: {0}".format(err))
            raise AgentUpdateError("Downloaded agent package: {0} is missing agent handler manifest file: {1}".format(agent_name, agent_handler_manifest_file))

    def download_and_get_new_agent(self, protocol, agent_family, goal_state):
        """
        Function downloads the new agent and returns the downloaded version.
        @param protocol: protocol object
        @param agent_family: agent family
        @param goal_state: goal state
        @return: GuestAgent: downloaded agent
        """
        if self._agent_manifest is None:  # Fetch agent manifest if it's not already done
            self._agent_manifest = goal_state.fetch_agent_manifest(agent_family.name, agent_family.uris)
        package_to_download = self._get_agent_package_to_download(self._agent_manifest, self._version)
        is_fast_track_goal_state = goal_state.extensions_goal_state.source == GoalStateSource.FastTrack
        self.download_new_agent_pkg(package_to_download, protocol, is_fast_track_goal_state)
        agent = GuestAgent.from_agent_package(package_to_download)
        return agent

    def purge_extra_agents_from_disk(self):
        """
        Remove the agents from disk except current version and new agent version
        """
        known_agents = [CURRENT_VERSION, self._version]
        self._purge_unknown_agents_from_disk(known_agents)

    def _get_agent_package_to_download(self, agent_manifest, version):
        """
        Returns the package of the given Version found in the manifest. If not found, returns exception
        """
        for pkg in agent_manifest.pkg_list.versions:
            if FlexibleVersion(pkg.version) == version:
                # Found a matching package, only download that one
                return pkg

        raise AgentUpdateError("No matching package found in the agent manifest for version: {0} in goal state incarnation: {1}, "
                        "skipping agent update".format(str(version), self._gs_id))

    @staticmethod
    def _purge_unknown_agents_from_disk(known_agents):
        """
        Remove from disk all directories and .zip files of unknown agents
        """
        path = os.path.join(conf.get_lib_dir(), "{0}-*".format(AGENT_NAME))

        for agent_path in glob.iglob(path):
            try:
                name = fileutil.trim_ext(agent_path, "zip")
                m = AGENT_DIR_PATTERN.match(name)
                if m is not None and FlexibleVersion(m.group(1)) not in known_agents:
                    if os.path.isfile(agent_path):
                        logger.info(u"Purging outdated Agent file {0}", agent_path)
                        os.remove(agent_path)
                    else:
                        logger.info(u"Purging outdated Agent directory {0}", agent_path)
                        shutil.rmtree(agent_path)
            except Exception as e:
                logger.warn(u"Purging {0} raised exception: {1}", agent_path, ustr(e))
