# 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.analyze

Created by Greg Neagle on 2017-01-10.

"""

import datetime
import os

from . import catalogs
from . import compare
from . import download
from . import installationstate
from . import manifestutils
from . import unused_software

from .. import display
from .. import fetch
from .. import info
from .. import munkilog
from .. import prefs
from .. import processes


def item_in_installinfo(item_pl, thelist, vers=''):
    """Determines if an item is in a list of processed items.

    Returns True if the item has already been processed (it's in the list)
    and, optionally, the version is the same or greater.
    """
    for listitem in thelist:
        try:
            if listitem['name'] == item_pl['name']:
                if not vers:
                    return True
                #if the version already installed or processed to be
                #installed is the same or greater, then we're good.
                if listitem.get('installed') and (compare.compare_versions(
                        listitem.get('installed_version'), vers) in (1, 2)):
                    return True
                if (compare.compare_versions(
                        listitem.get('version_to_install'), vers) in (1, 2)):
                    return True
        except KeyError:
            # item is missing 'name', so doesn't match
            pass

    return False


def is_apple_item(item_pl):
    """Returns True if the item to be installed or removed appears to be from
    Apple. If we are installing or removing any Apple items in a check/install
    cycle, we skip checking/installing Apple updates from an Apple Software
    Update server so we don't stomp on each other"""
    # is this a startosinstall item?
    if item_pl.get('installer_type') == 'startosinstall':
        return True
    # check receipts
    for receipt in item_pl.get('receipts', []):
        if receipt.get('packageid', '').startswith('com.apple.'):
            return True
    # check installs items
    for install_item in item_pl.get('installs', []):
        if install_item.get('CFBundleIdentifier', '').startswith('com.apple.'):
            return True
    # if we get here, no receipts or installs items have Apple
    # identifiers
    return False


def already_processed(itemname, installinfo, sections):
    '''Returns True if itemname has already been added to installinfo in one
    of the given sections'''
    description = {'processed_installs': 'install',
                   'processed_uninstalls': 'uninstall',
                   'managed_updates': 'update',
                   'optional_installs': 'optional install'}
    for section in sections:
        if itemname in installinfo[section]:
            display.display_debug1(
                '%s has already been processed for %s.',
                itemname, description[section])
            return True
    return False


def process_managed_update(manifestitem, cataloglist, installinfo):
    """Process a managed_updates item to see if it is installed, and if so,
    if it needs an update.
    """
    manifestitemname = os.path.split(manifestitem)[1]
    display.display_debug1(
        '* Processing manifest item %s for update', manifestitemname)

    if already_processed(
            manifestitemname, installinfo,
            ['managed_updates', 'processed_installs', 'processed_uninstalls']):
        return

    item_pl = catalogs.get_item_detail(manifestitem, cataloglist)
    if not item_pl:
        display.display_warning(
            'Could not process item %s for update. No pkginfo found in '
            'catalogs: %s ', manifestitem, ', '.join(cataloglist))
        return

    # we only offer to update if some version of the item is already
    # installed, so let's check
    if installationstate.some_version_installed(item_pl):
        # add to the list of processed managed_updates
        installinfo['managed_updates'].append(manifestitemname)
        dummy_result = process_install(
            manifestitem, cataloglist, installinfo, is_managed_update=True)
    else:
        display.display_debug1(
            '%s does not appear to be installed, so no managed updates...',
            manifestitemname)


def process_optional_install(manifestitem, cataloglist, installinfo):
    """Process an optional install item to see if it should be added to
    the list of optional installs.
    """
    manifestitemname = os.path.split(manifestitem)[1]
    display.display_debug1(
        "* Processing manifest item %s for optional install" % manifestitemname)

    if already_processed(
            manifestitemname, installinfo,
            ['optional_installs',
             'processed_installs', 'processed_uninstalls']):
        return

    # check to see if item (any version) is already in the
    # optional_install list:
    for item in installinfo['optional_installs']:
        if manifestitemname == item['name']:
            display.display_debug1(
                '%s has already been processed for optional install.',
                manifestitemname)
            return

    item_pl = catalogs.get_item_detail(manifestitem, cataloglist,
                                       suppress_warnings=True)
    if not item_pl and prefs.pref('ShowOptionalInstallsForHigherOSVersions'):
        # could not find an item valid for the current OS and hardware
        # try again to see if there is an item for a higher OS
        item_pl = catalogs.get_item_detail(
            manifestitem, cataloglist, skip_min_os_check=True,
            suppress_warnings=True)
        if item_pl:
            # found an item that requires a higher OS version
            display.display_debug1(
                'Found %s, version %s that requires a higher os version',
                item_pl['name'], item_pl['version'])
            # insert a note about the OS version requirement
            item_pl['note'] = ('Requires macOS version %s.'
                               % item_pl['minimum_os_version'])
            item_pl['update_available'] = True
    if not item_pl:
        # could not find anything that matches and is applicable
        display.display_warning(
            'Could not process item %s for optional install. No pkginfo '
            'found in catalogs: %s ', manifestitem, ', '.join(cataloglist))
        return

    is_currently_installed = installationstate.some_version_installed(item_pl)
    needs_update = False
    if is_currently_installed:
        if unused_software.should_be_removed(item_pl):
            process_removal(manifestitem, cataloglist, installinfo)
            manifestutils.remove_from_selfserve_installs(manifestitem)
            return
        if not 'installcheck_script' in item_pl:
            # installcheck_scripts can be expensive and only tell us if
            # an item is installed or not. So if iteminfo['installed'] is
            # True, and we're using an installcheck_script,
            # installationstate.installed_state is going to return 1
            # (which does not equal 0), so we can avoid running it again.
            # We should really revisit all of this in the future to avoid
            # repeated checks of the same data.
            # (installcheck_script isn't called if OnDemand is True, but if
            # OnDemand is true, is_currently_installed would be False, and
            # therefore we would not be here!)
            #
            # TL;DR: only check installed_state if no installcheck_script
            needs_update = installationstate.installed_state(item_pl) == 0

        if (not needs_update and
                prefs.pref('ShowOptionalInstallsForHigherOSVersions')):
            # the version we have installed is the newest for the current OS.
            # check again to see if there is a newer version for a higher OS
            display.display_debug1(
                'Checking for versions of %s that require a higher OS version',
                manifestitem)
            another_item_pl = catalogs.get_item_detail(
                manifestitem, cataloglist, skip_min_os_check=True,
                suppress_warnings=True)
            if another_item_pl != item_pl:
                # we found a different item. Replace the one we found
                # previously with this one.
                item_pl = another_item_pl
                display.display_debug1(
                    'Found %s, version %s that requires a higher os version',
                    item_pl['name'], item_pl['version'])
                # insert a note about the OS version requirement
                item_pl['note'] = ('Requires macOS version %s.'
                                   % item_pl['minimum_os_version'])
                item_pl['update_available'] = True

    # if we get to this point we can add this item
    # to the list of optional installs
    iteminfo = {}
    iteminfo['name'] = item_pl.get('name', manifestitemname)
    iteminfo['description'] = item_pl.get('description', '')
    iteminfo['version_to_install'] = item_pl.get('version', 'UNKNOWN')
    iteminfo['display_name'] = item_pl.get('display_name', '')
    for key in ['category', 'developer', 'featured', 'icon_name', 'icon_hash',
                'requires', 'RestartAction']:
        if key in item_pl:
            iteminfo[key] = item_pl[key]
    iteminfo['installed'] = is_currently_installed
    iteminfo['needs_update'] = needs_update
    iteminfo['licensed_seat_info_available'] = item_pl.get(
        'licensed_seat_info_available', False)
    iteminfo['uninstallable'] = (
        item_pl.get('uninstallable', False)
        and (item_pl.get('uninstall_method', '') != ''))
    iteminfo['installer_item_size'] = \
        item_pl.get('installer_item_size', 0)
    iteminfo['installed_size'] = item_pl.get(
        'installer_item_size', iteminfo['installer_item_size'])
    if item_pl.get('note'):
        # catalogs.get_item_detail() passed us a note about this item;
        # pass it along
        iteminfo['note'] = item_pl['note']
    elif needs_update or not is_currently_installed:
        if not download.enough_disk_space(
                item_pl, installinfo.get('managed_installs', []), warn=False):
            iteminfo['note'] = (
                'Insufficient disk space to download and install.')
            if needs_update:
                iteminfo['needs_update'] = False
                iteminfo['update_available'] = True
    optional_keys = ['preinstall_alert',
                     'preuninstall_alert',
                     'preupgrade_alert',
                     'OnDemand',
                     'minimum_os_version',
                     'update_available']
    for key in optional_keys:
        if key in item_pl:
            iteminfo[key] = item_pl[key]

    display.display_debug1(
        'Adding %s to the optional install list', iteminfo['name'])
    installinfo['optional_installs'].append(iteminfo)


def process_install(manifestitem, cataloglist, installinfo,
                    is_managed_update=False,
                    is_optional_install=False):
    """Processes a manifest item for install. Determines if it needs to be
    installed, and if so, if any items it is dependent on need to
    be installed first.  Installation detail is added to
    installinfo['managed_installs']
    Calls itself recursively as it processes dependencies.
    Returns a boolean; when processing dependencies, a false return
    will stop the installation of a dependent item
    """

    manifestitemname = os.path.split(manifestitem)[1]
    display.display_debug1(
        '* Processing manifest item %s for install', manifestitemname)
    (manifestitemname_withoutversion, includedversion) = (
        catalogs.split_name_and_version(manifestitemname))

    # have we processed this already?
    if manifestitemname in installinfo['processed_installs']:
        display.display_debug1(
            '%s has already been processed for install.', manifestitemname)
        return True
    elif (manifestitemname_withoutversion in
          installinfo['processed_uninstalls']):
        display.display_warning(
            'Will not process %s for install because it has already '
            'been processed for uninstall!', manifestitemname)
        return False

    item_pl = catalogs.get_item_detail(manifestitem, cataloglist)
    if not item_pl:
        display.display_warning(
            'Could not process item %s for install. No pkginfo found in '
            'catalogs: %s ', manifestitem, ', '.join(cataloglist))
        return False
    elif is_managed_update:
        # we're processing this as a managed update, so don't
        # add it to the processed_installs list
        pass
    else:
        # we found it, so add it to our list of processed installs
        # so we don't process it again in the future
        display.display_debug2(
            'Adding %s to list of processed installs' % manifestitemname)
        installinfo['processed_installs'].append(manifestitemname)

    if item_in_installinfo(item_pl, installinfo['managed_installs'],
                           vers=item_pl.get('version')):
        # has this item already been added to the list of things to install?
        display.display_debug1(
            '%s is or will be installed.', manifestitemname)
        return True

    # check dependencies
    dependencies_met = True

    # there are two kinds of dependencies/relationships.
    #
    # 'requires' are prerequistes:
    #  package A requires package B be installed first.
    #  if package A is removed, package B is unaffected.
    #  requires can be a one to many relationship.
    #
    #  The second type of relationship is 'update_for'.
    #  This signifies that that current package should be considered an update
    #  for the packages listed in the 'update_for' array. When processing a
    #  package, we look through the catalogs for other packages that declare
    #  they are updates for the current package and install them if needed.
    #  This can be a one-to-many relationship - one package can be an update
    #  for several other packages; for example, 'PhotoshopCS4update-11.0.1'
    #  could be an update for PhotoshopCS4 and for AdobeCS4DesignSuite.
    #
    #  When removing an item, any updates for that item are removed as well.

    if 'requires' in item_pl:
        dependencies = item_pl['requires']
        # fix things if 'requires' was specified as a string
        # instead of an array of strings
        if isinstance(dependencies, basestring):
            dependencies = [dependencies]
        for item in dependencies:
            display.display_detail(
                '%s-%s requires %s. Getting info on %s...'
                % (item_pl.get('name', manifestitemname),
                   item_pl.get('version', ''), item, item))
            success = process_install(item, cataloglist, installinfo,
                                      is_managed_update=is_managed_update)
            if not success:
                dependencies_met = False

    iteminfo = {}
    iteminfo['name'] = item_pl.get('name', '')
    iteminfo['display_name'] = item_pl.get('display_name', iteminfo['name'])
    iteminfo['description'] = item_pl.get('description', '')

    if not dependencies_met:
        display.display_warning(
            'Didn\'t attempt to install %s because could not resolve all '
            'dependencies.', manifestitemname)
        # add information to managed_installs so we have some feedback
        # to display in MSC.app
        iteminfo['installed'] = False
        iteminfo['note'] = ('Can\'t install %s because could not resolve all '
                            'dependencies.' % iteminfo['display_name'])
        iteminfo['version_to_install'] = item_pl.get('version', 'UNKNOWN')
        installinfo['managed_installs'].append(iteminfo)
        return False

    installed_state = installationstate.installed_state(item_pl)
    if installed_state == 0:
        display.display_detail('Need to install %s', manifestitemname)
        iteminfo['installer_item_size'] = item_pl.get(
            'installer_item_size', 0)
        iteminfo['installed_size'] = item_pl.get(
            'installed_size', iteminfo['installer_item_size'])
        try:
            # Get a timestamp, then download the installer item.
            start = datetime.datetime.now()
            if item_pl.get('installer_type', 0) == 'nopkg':
                # Packageless install
                download_speed = 0
                filename = 'packageless_install'
            else:
                if download.download_installeritem(item_pl, installinfo):
                    # Record the download speed to the InstallResults output.
                    end = datetime.datetime.now()
                    download_seconds = (end - start).seconds
                    try:
                        if iteminfo['installer_item_size'] < 1024:
                            # ignore downloads under 1 MB or speeds will
                            # be skewed.
                            download_speed = 0
                        else:
                            # installer_item_size is KBytes, so divide
                            # by seconds.
                            download_speed = int(
                                iteminfo['installer_item_size'] /
                                download_seconds)
                    except (TypeError, ValueError, ZeroDivisionError):
                        download_speed = 0
                else:
                    # Item was already in cache; set download_speed to 0.
                    download_speed = 0

                filename = download.get_url_basename(
                    item_pl['installer_item_location'])

            iteminfo['download_kbytes_per_sec'] = download_speed
            if download_speed:
                display.display_detail(
                    '%s downloaded at %d KB/s', filename, download_speed)

            # required keys
            iteminfo['installer_item'] = filename
            iteminfo['installed'] = False
            iteminfo['version_to_install'] = item_pl.get('version', 'UNKNOWN')

            # we will ignore the unattended_install key if the item needs a
            # restart or logout...
            if (item_pl.get('unattended_install') or
                    item_pl.get('forced_install')):
                if item_pl.get('RestartAction', 'None') != 'None':
                    display.display_warning(
                        'Ignoring unattended_install key for %s because '
                        'RestartAction is %s.',
                        item_pl['name'], item_pl.get('RestartAction'))
                else:
                    iteminfo['unattended_install'] = True

            # optional keys to copy if they exist
            optional_keys = ['additional_startosinstall_options',
                             'allow_untrusted',
                             'suppress_bundle_relocation',
                             'installer_choices_xml',
                             'installer_environment',
                             'adobe_install_info',
                             'RestartAction',
                             'installer_type',
                             'adobe_package_name',
                             'package_path',
                             'blocking_applications',
                             'installs',
                             'requires',
                             'update_for',
                             'payloads',
                             'preinstall_script',
                             'postinstall_script',
                             'items_to_copy',  # used w/ copy_from_dmg
                             'copy_local',     # used w/ AdobeCS5 Updaters
                             'force_install_after_date',
                             'apple_item',
                             'category',
                             'developer',
                             'icon_name',
                             'PayloadIdentifier',
                             'icon_hash',
                             'OnDemand']

            if (is_optional_install and
                    not installationstate.some_version_installed(item_pl)):
                # For optional installs where no version is installed yet
                # we do not enforce force_install_after_date
                optional_keys.remove('force_install_after_date')

            for key in optional_keys:
                if key in item_pl:
                    iteminfo[key] = item_pl[key]

            if 'apple_item' not in iteminfo:
                # admin did not explicitly mark this item; let's determine if
                # it's from Apple
                if is_apple_item(item_pl):
                    munkilog.log(
                        'Marking %s as apple_item - this will block '
                        'Apple SUS updates' % iteminfo['name'])
                    iteminfo['apple_item'] = True

            installinfo['managed_installs'].append(iteminfo)

            update_list = []
            # (manifestitemname_withoutversion, includedversion) =
            # nameAndVersion(manifestitemname)
            if includedversion:
                # a specific version was specified in the manifest
                # so look only for updates for this specific version
                update_list = catalogs.look_for_updates_for_version(
                    manifestitemname_withoutversion,
                    includedversion, cataloglist)
            else:
                # didn't specify a specific version, so
                # now look for all updates for this item
                update_list = catalogs.look_for_updates(
                    manifestitemname_withoutversion, cataloglist)
                # now append any updates specifically
                # for the version to be installed
                update_list.extend(
                    catalogs.look_for_updates_for_version(
                        manifestitemname_withoutversion,
                        iteminfo['version_to_install'],
                        cataloglist))

            for update_item in update_list:
                # call processInstall recursively so we get the
                # latest version and dependencies
                dummy_result = process_install(
                    update_item, cataloglist, installinfo,
                    is_managed_update=is_managed_update)
            return True
        except fetch.PackageVerificationError:
            display.display_warning(
                'Can\'t install %s because the integrity check failed.',
                manifestitem)
            iteminfo['installed'] = False
            iteminfo['note'] = 'Integrity check failed'
            iteminfo['version_to_install'] = item_pl.get('version', 'UNKNOWN')
            for key in ['developer', 'icon_name']:
                if key in item_pl:
                    iteminfo[key] = item_pl[key]
            installinfo['managed_installs'].append(iteminfo)
            #if manifestitemname in installinfo['processed_installs']:
            #    installinfo['processed_installs'].remove(manifestitemname)
            return False
        except (fetch.GurlError, fetch.GurlDownloadError), errmsg:
            display.display_warning(
                'Download of %s failed: %s', manifestitem, errmsg)
            iteminfo['installed'] = False
            iteminfo['note'] = u'Download failed (%s)' % errmsg
            iteminfo['version_to_install'] = item_pl.get('version', 'UNKNOWN')
            for key in ['developer', 'icon_name']:
                if key in item_pl:
                    iteminfo[key] = item_pl[key]
            installinfo['managed_installs'].append(iteminfo)
            #if manifestitemname in installinfo['processed_installs']:
            #    installinfo['processed_installs'].remove(manifestitemname)
            return False
        except fetch.Error, errmsg:
            display.display_warning(
                'Can\'t install %s because: %s', manifestitemname, errmsg)
            iteminfo['installed'] = False
            iteminfo['note'] = '%s' % errmsg
            iteminfo['version_to_install'] = item_pl.get('version', 'UNKNOWN')
            for key in ['developer', 'icon_name']:
                if key in item_pl:
                    iteminfo[key] = item_pl[key]
            installinfo['managed_installs'].append(iteminfo)
            #if manifestitemname in installinfo['processed_installs']:
            #    installinfo['processed_installs'].remove(manifestitemname)
            return False
    else:
        iteminfo['installed'] = True
        # record installed size for reporting
        iteminfo['installed_size'] = item_pl.get(
            'installed_size', item_pl.get('installer_item_size', 0))
        if installed_state == 1:
            # just use the version from the pkginfo
            iteminfo['installed_version'] = item_pl['version']
        else:
            # might be newer; attempt to figure out the version
            installed_version = compare.get_installed_version(item_pl)
            if installed_version == "UNKNOWN":
                installed_version = '(newer than %s)' % item_pl['version']
            iteminfo['installed_version'] = installed_version
        installinfo['managed_installs'].append(iteminfo)
        # remove included version number if any
        (name, includedversion) = catalogs.split_name_and_version(
            manifestitemname)
        display.display_detail('%s version %s (or newer) is already installed.',
                               name, item_pl['version'])
        update_list = []
        if not includedversion:
            # no specific version is specified;
            # the item is already installed;
            # now look for updates for this item
            update_list = catalogs.look_for_updates(name, cataloglist)
            # and also any for this specific version
            installed_version = iteminfo['installed_version']
            if not installed_version.startswith('(newer than '):
                update_list.extend(
                    catalogs.look_for_updates_for_version(
                        name, installed_version, cataloglist))
        elif compare.compare_versions(
                includedversion, iteminfo['installed_version']) == 1:
            # manifest specifies a specific version
            # if that's what's installed, look for any updates
            # specific to this version
            update_list = catalogs.look_for_updates_for_version(
                manifestitemname_withoutversion, includedversion, cataloglist)
        # if we have any updates, process them
        for update_item in update_list:
            # call processInstall recursively so we get updates
            # and any dependencies
            dummy_result = process_install(
                update_item, cataloglist, installinfo,
                is_managed_update=is_managed_update)

        return True


def process_manifest_for_key(manifest, manifest_key, installinfo,
                             parentcatalogs=None):
    """Processes keys in manifests to build the lists of items to install and
    remove.

    Can be recursive if manifests include other manifests.
    Probably doesn't handle circular manifest references well.

    manifest can be a path to a manifest file or a dictionary object.
    """
    if isinstance(manifest, basestring):
        display.display_debug1(
            "** Processing manifest %s for %s" %
            (os.path.basename(manifest), manifest_key))
        manifestdata = manifestutils.get_manifest_data(manifest)
    else:
        manifestdata = manifest
        manifest = 'embedded manifest'

    cataloglist = manifestdata.get('catalogs')
    if cataloglist:
        catalogs.get_catalogs(cataloglist)
    elif parentcatalogs:
        cataloglist = parentcatalogs

    if not cataloglist:
        display.display_warning('Manifest %s has no catalogs', manifest)
        return

    for item in manifestdata.get('included_manifests', []):
        nestedmanifestpath = manifestutils.get_manifest(item)
        if not nestedmanifestpath:
            raise manifestutils.ManifestException
        if processes.stop_requested():
            return {}
        process_manifest_for_key(nestedmanifestpath, manifest_key,
                                 installinfo, cataloglist)

    conditionalitems = manifestdata.get('conditional_items', [])
    if conditionalitems:
        display.display_debug1(
            '** Processing conditional_items in %s', manifest)
    # conditionalitems should be an array of dicts
    # each dict has a predicate; the rest consists of the
    # same keys as a manifest
    for item in conditionalitems:
        try:
            predicate = item['condition']
        except (AttributeError, KeyError):
            display.display_warning(
                'Missing predicate for conditional_item %s', item)
            continue
        except BaseException:
            display.display_warning(
                'Conditional item is malformed: %s', item)
            continue
        if info.predicate_evaluates_as_true(
                predicate, additional_info={'catalogs': cataloglist}):
            conditionalmanifest = item
            process_manifest_for_key(
                conditionalmanifest, manifest_key, installinfo, cataloglist)

    for item in manifestdata.get(manifest_key, []):
        if processes.stop_requested():
            return {}
        if manifest_key == 'managed_installs':
            dummy_result = process_install(item, cataloglist, installinfo)
        elif manifest_key == 'managed_updates':
            process_managed_update(item, cataloglist, installinfo)
        elif manifest_key == 'optional_installs':
            process_optional_install(item, cataloglist, installinfo)
        elif manifest_key == 'managed_uninstalls':
            dummy_result = process_removal(item, cataloglist, installinfo)
        elif manifest_key == 'featured_items':
            installinfo['featured_items'].append(item)


def process_removal(manifestitem, cataloglist, installinfo):
    """Processes a manifest item; attempts to determine if it
    needs to be removed, and if it can be removed.

    Unlike installs, removals aren't really version-specific -
    If we can figure out how to remove the currently installed
    version, we do, unless the admin specifies a specific version
    number in the manifest. In that case, we only attempt a
    removal if the version installed matches the specific version
    in the manifest.

    Any items dependent on the given item need to be removed first.
    Items to be removed are added to installinfo['removals'].

    Calls itself recursively as it processes dependencies.
    Returns a boolean; when processing dependencies, a false return
    will stop the removal of a dependent item.
    """
    def get_receipts_to_remove(item):
        """Returns a list of receipts to remove for item"""
        name = item['name']
        pkgdata = catalogs.analyze_installed_pkgs()
        if name in pkgdata['receipts_for_name']:
            return pkgdata['receipts_for_name'][name]
        return []

    manifestitemname_withversion = os.path.split(manifestitem)[1]
    display.display_debug1(
        '* Processing manifest item %s for removal' %
        manifestitemname_withversion)

    (manifestitemname, includedversion) = catalogs.split_name_and_version(
        manifestitemname_withversion)

    # have we processed this already?
    if manifestitemname in [catalogs.split_name_and_version(item)[0]
                            for item in installinfo['processed_installs']]:
        display.display_warning(
            'Will not attempt to remove %s because some version of it is in '
            'the list of managed installs, or it is required by another'
            ' managed install.', manifestitemname)
        return False
    elif manifestitemname in installinfo['processed_uninstalls']:
        display.display_debug1(
            '%s has already been processed for removal.', manifestitemname)
        return True
    else:
        installinfo['processed_uninstalls'].append(manifestitemname)

    infoitems = []
    if includedversion:
        # a specific version was specified
        item_pl = catalogs.get_item_detail(
            manifestitemname, cataloglist, includedversion)
        if item_pl:
            infoitems.append(item_pl)
    else:
        # get all items matching the name provided
        infoitems = catalogs.get_all_items_with_name(
            manifestitemname, cataloglist)

    if not infoitems:
        display.display_warning(
            'Could not process item %s for removal. No pkginfo found in '
            'catalogs: %s ', manifestitemname, ', '.join(cataloglist))
        return False

    install_evidence = False
    for item in infoitems:
        display.display_debug2('Considering item %s-%s for removal info',
                               item['name'], item['version'])
        if installationstate.evidence_this_is_installed(item):
            install_evidence = True
            break
        else:
            display.display_debug2(
                '%s-%s not installed.', item['name'], item['version'])

    if not install_evidence:
        display.display_detail(
            '%s doesn\'t appear to be installed.', manifestitemname_withversion)
        iteminfo = {}
        iteminfo['name'] = manifestitemname
        iteminfo['installed'] = False
        installinfo['removals'].append(iteminfo)
        return True

    # if we get here, install_evidence is true, and item
    # holds the item we found install evidence for, so we
    # should use that item to do the removal
    uninstall_item = None
    packages_to_remove = []
    # check for uninstall info
    # and grab the first uninstall method we find.
    if item.get('uninstallable') and 'uninstall_method' in item:
        uninstallmethod = item['uninstall_method']
        if uninstallmethod == 'removepackages':
            packages_to_remove = get_receipts_to_remove(item)
            if packages_to_remove:
                uninstall_item = item
        elif uninstallmethod.startswith('Adobe'):
            # Adobe CS3/CS4/CS5/CS6/CC product
            uninstall_item = item
        elif uninstallmethod in ['remove_copied_items',
                                 'remove_app',
                                 'uninstall_script',
                                 'remove_profile']:
            uninstall_item = item
        else:
            # uninstall_method is a local script.
            # Check to see if it exists and is executable
            if os.path.exists(uninstallmethod) and \
               os.access(uninstallmethod, os.X_OK):
                uninstall_item = item

    if not uninstall_item:
        # the uninstall info for the item couldn't be matched
        # to what's on disk
        display.display_warning('Could not find uninstall info for %s.',
                                manifestitemname_withversion)
        return False

    # if we got this far, we have enough info to attempt an uninstall.
    # the pkginfo is in uninstall_item
    # Now check for dependent items
    #
    # First, look through catalogs for items that are required by this item;
    # if any are installed, we need to remove them as well
    #
    # still not sure how to handle references to specific versions --
    # if another package says it requires SomePackage--1.0.0.0.0
    # and we're supposed to remove SomePackage--1.0.1.0.0... what do we do?
    #
    dependentitemsremoved = True

    uninstall_item_name = uninstall_item.get('name')
    uninstall_name_w_version = (
        '%s-%s' % (uninstall_item.get('name'), uninstall_item.get('version')))
    alt_uninstall_name_w_version = (
        '%s--%s' % (uninstall_item.get('name'), uninstall_item.get('version')))
    processednames = []
    for catalogname in cataloglist:
        if not catalogname in catalogs.catalogs():
            # in case the list refers to a non-existent catalog
            continue
        for item_pl in catalogs.catalogs()[catalogname]['items']:
            name = item_pl.get('name')
            if name not in processednames:
                if 'requires' in item_pl:
                    if (uninstall_item_name in item_pl['requires'] or
                            uninstall_name_w_version
                            in item_pl['requires'] or
                            alt_uninstall_name_w_version
                            in item_pl['requires']):
                        display.display_debug1(
                            '%s requires %s, checking to see if it\'s '
                            'installed...', item_pl.get('name'),
                            manifestitemname)
                        if installationstate.evidence_this_is_installed(
                                item_pl):
                            display.display_detail(
                                '%s requires %s. %s must be removed as well.',
                                item_pl.get('name'), manifestitemname,
                                item_pl.get('name'))
                            success = process_removal(
                                item_pl.get('name'), cataloglist, installinfo)
                            if not success:
                                dependentitemsremoved = False
                                break
                # record this name so we don't process it again
                processednames.append(name)

    if not dependentitemsremoved:
        display.display_warning('Will not attempt to remove %s because could '
                                'not remove all items dependent on it.',
                                manifestitemname_withversion)
        return False

    # Finally! We can record the removal information!
    iteminfo = {}
    iteminfo['name'] = uninstall_item.get('name', '')
    iteminfo['display_name'] = uninstall_item.get('display_name', '')
    iteminfo['description'] = 'Will be removed.'

    # we will ignore the unattended_uninstall key if the item needs a restart
    # or logout...
    if (uninstall_item.get('unattended_uninstall') or
            uninstall_item.get('forced_uninstall')):
        if uninstall_item.get('RestartAction', 'None') != 'None':
            display.display_warning(
                'Ignoring unattended_uninstall key for %s '
                'because RestartAction is %s.',
                uninstall_item['name'],
                uninstall_item.get('RestartAction'))
        else:
            iteminfo['unattended_uninstall'] = True

    # some keys we'll copy if they exist
    optional_keys = ['blocking_applications',
                     'installs',
                     'requires',
                     'update_for',
                     'payloads',
                     'preuninstall_script',
                     'postuninstall_script',
                     'apple_item',
                     'category',
                     'developer',
                     'icon_name',
                     'PayloadIdentifier']
    for key in optional_keys:
        if key in uninstall_item:
            iteminfo[key] = uninstall_item[key]

    if 'apple_item' not in iteminfo:
        # admin did not explicitly mark this item; let's determine if
        # it's from Apple
        if is_apple_item(item_pl):
            iteminfo['apple_item'] = True

    if packages_to_remove:
        # remove references for each package
        packages_to_really_remove = []
        for pkg in packages_to_remove:
            display.display_debug1('Considering %s for removal...', pkg)
            # find pkg in pkgdata['pkg_references'] and remove the reference
            # so we only remove packages if we're the last reference to it
            pkgdata = catalogs.analyze_installed_pkgs()
            if pkg in pkgdata['pkg_references']:
                display.display_debug1('%s references are: %s', pkg,
                                       pkgdata['pkg_references'][pkg])
                if iteminfo['name'] in pkgdata['pkg_references'][pkg]:
                    pkgdata['pkg_references'][pkg].remove(iteminfo['name'])
                    if len(pkgdata['pkg_references'][pkg]) == 0:
                        display.display_debug1(
                            'Adding %s to removal list.', pkg)
                        packages_to_really_remove.append(pkg)
            else:
                # This shouldn't happen
                display.display_warning(
                    'pkg id %s missing from pkgdata', pkg)
        if packages_to_really_remove:
            iteminfo['packages'] = packages_to_really_remove
        else:
            # no packages that belong to this item only.
            display.display_warning('could not find unique packages to remove '
                                    'for %s', iteminfo['name'])
            return False

    iteminfo['uninstall_method'] = uninstallmethod
    if uninstallmethod.startswith('Adobe'):
        if (uninstallmethod == "AdobeCS5AAMEEPackage" and
                'adobe_install_info' in item):
            iteminfo['adobe_install_info'] = item['adobe_install_info']
        else:
            if 'uninstaller_item_location' in item:
                location = uninstall_item['uninstaller_item_location']
            else:
                location = uninstall_item['installer_item_location']
            try:
                download.download_installeritem(
                    item, installinfo, uninstalling=True)
                filename = os.path.split(location)[1]
                iteminfo['uninstaller_item'] = filename
                iteminfo['adobe_package_name'] = uninstall_item.get(
                    'adobe_package_name', '')
            except fetch.PackageVerificationError:
                display.display_warning(
                    'Can\'t uninstall %s because the integrity check '
                    'failed.', iteminfo['name'])
                return False
            except fetch.Error, errmsg:
                display.display_warning(
                    'Failed to download the uninstaller for %s because %s',
                    iteminfo['name'], errmsg)
                return False
    elif uninstallmethod == 'remove_copied_items':
        iteminfo['items_to_remove'] = item.get('items_to_copy', [])
    elif uninstallmethod == 'remove_app':
        if uninstall_item.get('installs', None):
            iteminfo['remove_app_info'] = uninstall_item['installs'][0]
    elif uninstallmethod == 'uninstall_script':
        iteminfo['uninstall_script'] = item.get('uninstall_script', '')

    # before we add this removal to the list,
    # check for installed updates and add them to the
    # removal list as well:
    update_list = catalogs.look_for_updates(uninstall_item_name, cataloglist)
    update_list.extend(catalogs.look_for_updates(
        uninstall_name_w_version, cataloglist))
    update_list.extend(catalogs.look_for_updates(
        alt_uninstall_name_w_version, cataloglist))
    for update_item in update_list:
        # call us recursively...
        dummy_result = process_removal(update_item, cataloglist, installinfo)

    # finish recording info for this removal
    iteminfo['installed'] = True
    iteminfo['installed_version'] = uninstall_item.get('version')
    if 'RestartAction' in uninstall_item:
        iteminfo['RestartAction'] = uninstall_item['RestartAction']
    installinfo['removals'].append(iteminfo)
    display.display_detail(
        'Removal of %s added to ManagedInstaller tasks.',
        manifestitemname_withversion)
    return True


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