# encoding: utf-8
#
# Copyright 2009-2017 Greg Neagle.
#
# 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
#
#      https://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.
"""
updatecheck.download

Created by Greg Neagle on 2016-12-31.


Functions for downloading resources from the Munki server
"""

import os
import urllib2
import urlparse

from .. import display
from .. import fetch
from .. import info
from .. import munkihash
from .. import osutils
from .. import prefs
from .. import reports
from .. import FoundationPlist


ICON_HASHES_PLIST_NAME = '_icon_hashes.plist'


def enough_disk_space(item_pl, installlist=None, uninstalling=False, warn=True):
    """Determine if there is enough disk space to download the installer
    item."""
    # fudgefactor is set to 100MB
    fudgefactor = 102400
    alreadydownloadedsize = 0
    if 'installer_item_location' in item_pl:
        download = get_download_cache_path(item_pl['installer_item_location'])
        if os.path.exists(download):
            alreadydownloadedsize = os.path.getsize(download)
    installeritemsize = int(item_pl.get('installer_item_size', 0))
    installedsize = int(item_pl.get('installed_size', installeritemsize))
    if uninstalling:
        installedsize = 0
        if 'uninstaller_item_size' in item_pl:
            installeritemsize = int(item_pl['uninstaller_item_size'])
    diskspaceneeded = (installeritemsize - alreadydownloadedsize +
                       installedsize + fudgefactor)

    # info.available_disk_space() returns KB
    availablediskspace = info.available_disk_space()
    if installlist:
        for item in installlist:
            # subtract space needed for other items that are to be installed
            if item.get('installer_item'):
                availablediskspace = availablediskspace - \
                                     int(item.get('installed_size', 0))

    if availablediskspace > diskspaceneeded:
        return True
    elif warn:
        if uninstalling:
            display.display_warning('There is insufficient disk space to '
                                    'download the uninstaller for %s.',
                                    item_pl.get('name'))
        else:
            display.display_warning('There is insufficient disk space to '
                                    'download and install %s.',
                                    item_pl.get('name'))
        display.display_warning(
            '    %sMB needed; %sMB available',
            diskspaceneeded/1024, availablediskspace/1024)
    return False


def get_url_basename(url):
    """For a URL, absolute or relative, return the basename string.

    e.g. "http://foo/bar/path/foo.dmg" => "foo.dmg"
         "/path/foo.dmg" => "foo.dmg"
    """

    url_parse = urlparse.urlparse(url)
    return os.path.basename(url_parse.path)


def get_download_cache_path(url):
    """For a URL, return the path that the download should cache to.

    Returns a string."""
    cachedir = os.path.join(prefs.pref('ManagedInstallDir'), 'Cache')
    return os.path.join(cachedir, get_url_basename(url))


def download_installeritem(item_pl, installinfo, uninstalling=False):
    """Downloads an (un)installer item.
    Returns True if the item was downloaded, False if it was already cached.
    Raises an error if there are issues..."""

    download_item_key = 'installer_item_location'
    item_hash_key = 'installer_item_hash'
    if uninstalling and 'uninstaller_item_location' in item_pl:
        download_item_key = 'uninstaller_item_location'
        item_hash_key = 'uninstaller_item_hash'

    location = item_pl.get(download_item_key)
    if not location:
        raise fetch.DownloadError(
            "No %s in item info." % download_item_key)

    # allow pkginfo preferences to override system munki preferences
    downloadbaseurl = item_pl.get('PackageCompleteURL') or \
                      item_pl.get('PackageURL') or \
                      prefs.pref('PackageURL') or \
                      prefs.pref('SoftwareRepoURL') + '/pkgs/'

    # build a URL, quoting the the location to encode reserved characters
    if item_pl.get('PackageCompleteURL'):
        pkgurl = downloadbaseurl
    else:
        if not downloadbaseurl.endswith('/'):
            downloadbaseurl = downloadbaseurl + '/'
        pkgurl = downloadbaseurl + urllib2.quote(location.encode('UTF-8'))

    pkgname = get_url_basename(location)
    display.display_debug2('Download base URL is: %s', downloadbaseurl)
    display.display_debug2('Package name is: %s', pkgname)
    display.display_debug2('Download URL is: %s', pkgurl)

    destinationpath = get_download_cache_path(location)
    display.display_debug2('Downloading to: %s', destinationpath)

    display.display_detail('Downloading %s from %s', pkgname, location)

    if not os.path.exists(destinationpath):
        # check to see if there is enough free space to download and install
        if not enough_disk_space(item_pl, installinfo['managed_installs'],
                                 uninstalling=uninstalling):
            raise fetch.DownloadError(
                'Insufficient disk space to download and install %s' % pkgname)
        else:
            display.display_detail(
                'Downloading %s from %s', pkgname, location)

    dl_message = 'Downloading %s...' % pkgname
    expected_hash = item_pl.get(item_hash_key, None)
    return fetch.munki_resource(pkgurl, destinationpath,
                                resume=True,
                                message=dl_message,
                                expected_hash=expected_hash,
                                verify=True)


def clean_up_icons_dir(icons_to_keep):
    '''Remove any cached/downloaded icons that aren't in the list of ones to
    keep'''
    # remove no-longer needed icons from the local directory
    icons_to_keep.append(ICON_HASHES_PLIST_NAME)
    icon_dir = os.path.join(prefs.pref('ManagedInstallDir'), 'icons')
    for (dirpath, dummy_dirnames, filenames) in os.walk(
            icon_dir, topdown=False):
        for filename in filenames:
            icon_path = os.path.join(dirpath, filename)
            rel_path = icon_path[len(icon_dir):].lstrip('/')
            if rel_path not in icons_to_keep:
                try:
                    os.unlink(icon_path)
                except (IOError, OSError):
                    pass
        if len(osutils.listdir(dirpath)) == 0:
            # did we empty out this directory (or is it already empty)?
            # if so, remove it
            try:
                os.rmdir(dirpath)
            except (IOError, OSError):
                pass


def get_icon_hashes(icon_base_url):
    '''Attempts to download the list of compiled icon hashes'''
    icon_hashes = None
    icon_hashes_url = icon_base_url + ICON_HASHES_PLIST_NAME
    icon_dir = os.path.join(prefs.pref('ManagedInstallDir'), 'icons')
    icon_hashes_plist = os.path.join(icon_dir, ICON_HASHES_PLIST_NAME)
    try:
        fetch.munki_resource(icon_hashes_url, icon_hashes_plist,
                             message="Getting list of available icons")
        icon_hashes = FoundationPlist.readPlist(icon_hashes_plist)
    except (fetch.Error, FoundationPlist.FoundationPlistException):
        pass
    return icon_hashes


def download_icons(item_list):
    '''Attempts to download icons (actually image files) for items in
       item_list'''
    icons_to_keep = []
    icon_known_exts = ['.bmp', '.gif', '.icns', '.jpg', '.jpeg', '.png', '.psd',
                       '.tga', '.tif', '.tiff', '.yuv']
    icon_base_url = (prefs.pref('IconURL') or
                     prefs.pref('SoftwareRepoURL') + '/icons/')
    # make sure the icon_base_url ends with exactly one slash
    icon_base_url = icon_base_url.rstrip('/') + '/'
    display.display_debug2('Icon base URL is: %s', icon_base_url)
    icon_dir = os.path.join(prefs.pref('ManagedInstallDir'), 'icons')
    icon_hashes = get_icon_hashes(icon_base_url)

    for item in item_list:
        icon_name = item.get('icon_name') or item['name']
        if not os.path.splitext(icon_name)[1] in icon_known_exts:
            icon_name += '.png'
        server_icon_hash = item.get('icon_hash')
        if not server_icon_hash and icon_hashes:
            server_icon_hash = icon_hashes.get(icon_name)
        icons_to_keep.append(icon_name)
        icon_path = os.path.join(icon_dir, icon_name)
        if os.path.isfile(icon_path):
            # have we already downloaded it? If so get the hash
            local_hash = fetch.getxattr(icon_path, fetch.XATTR_SHA)
            if not local_hash:
                local_hash = munkihash.getsha256hash(icon_path)
                fetch.writeCachedChecksum(icon_path, local_hash)
        else:
            local_hash = 'nonexistent'
        icon_subdir = os.path.dirname(icon_path)
        if not os.path.isdir(icon_subdir):
            try:
                os.makedirs(icon_subdir, 0755)
            except OSError, err:
                display.display_error('Could not create %s' % icon_subdir)
                return
        if server_icon_hash != local_hash:
            # hashes don't match, so download the icon
            if icon_hashes and icon_name not in icon_hashes:
                # if we have a list of icon hashes, and the icon name is not
                # in that list, then there's no point in attempting to
                # download this icon
                continue
            item_name = item.get('display_name') or item['name']
            message = 'Getting icon %s for %s...' % (icon_name, item_name)
            icon_url = icon_base_url + urllib2.quote(icon_name.encode('UTF-8'))
            try:
                fetch.munki_resource(
                    icon_url, icon_path, message=message)
            except fetch.Error, err:
                display.display_debug1(
                    'Could not retrieve icon %s from the server: %s',
                    icon_name, err)
            else:
                # if we downloaded it, store the hash for later use
                if os.path.isfile(icon_path):
                    fetch.writeCachedChecksum(icon_path)

    # delete any previously downloaded icons we no longer need
    clean_up_icons_dir(icons_to_keep)


def download_client_resources():
    """Download client customization resources (if any)."""
    # Munki's preferences can specify an explicit name
    # under ClientResourcesFilename
    # if that doesn't exist, use the primary manifest name as the
    # filename. If that fails, try site_default.zip
    filenames = []
    resources_name = prefs.pref('ClientResourcesFilename')
    if resources_name:
        if os.path.splitext(resources_name)[1] != '.zip':
            resources_name += '.zip'
        filenames.append(resources_name)
    else:
        filenames.append(reports.report['ManifestName'] + '.zip')
    filenames.append('site_default.zip')

    resource_base_url = (
        prefs.pref('ClientResourceURL') or
        prefs.pref('SoftwareRepoURL') + '/client_resources/')
    resource_base_url = resource_base_url.rstrip('/') + '/'
    resource_dir = os.path.join(
        prefs.pref('ManagedInstallDir'), 'client_resources')
    display.display_debug2(
        'Client resources base URL is: %s', resource_base_url)
    # make sure local resource directory exists
    if not os.path.isdir(resource_dir):
        try:
            os.makedirs(resource_dir, 0755)
        except OSError, err:
            display.display_error(
                'Could not create %s' % resource_dir)
            return
    resource_archive_path = os.path.join(resource_dir, 'custom.zip')
    message = 'Getting client resources...'
    downloaded_resource_path = None
    for filename in filenames:
        resource_url = resource_base_url + urllib2.quote(
            filename.encode('UTF-8'))
        try:
            fetch.munki_resource(
                resource_url, resource_archive_path, message=message)
            downloaded_resource_path = resource_archive_path
            break
        except fetch.Error, err:
            display.display_debug1(
                'Could not retrieve client resources with name %s: %s',
                filename, err)
    if downloaded_resource_path is None:
        # make sure we don't have an old custom.zip hanging around
        if os.path.exists(resource_archive_path):
            try:
                os.unlink(resource_archive_path)
            except (OSError, IOError), err:
                display.display_error(
                    'Could not remove stale %s: %s', resource_archive_path, err)


def download_catalog(catalogname):
    '''Attempt to download a catalog from the Munki server, Returns the path to
    the downlaoded catalog file'''
    catalogbaseurl = (prefs.pref('CatalogURL') or
                      prefs.pref('SoftwareRepoURL') + '/catalogs/')
    if not catalogbaseurl.endswith('?') and not catalogbaseurl.endswith('/'):
        catalogbaseurl = catalogbaseurl + '/'
    display.display_debug2('Catalog base URL is: %s', catalogbaseurl)
    catalog_dir = os.path.join(prefs.pref('ManagedInstallDir'), 'catalogs')
    catalogurl = catalogbaseurl + urllib2.quote(catalogname.encode('UTF-8'))
    catalogpath = os.path.join(catalog_dir, catalogname)
    display.display_detail('Getting catalog %s...', catalogname)
    message = 'Retrieving catalog "%s"...' % catalogname
    try:
        fetch.munki_resource(catalogurl, catalogpath, message=message)
        return catalogpath
    except fetch.Error, err:
        display.display_error(
            'Could not retrieve catalog %s from server: %s',
            catalogname, err)
        return None


if __name__ == '__main__':
    print 'This is a library of support tools for the Munki Suite.'
