#!/usr/bin/python
# encoding: utf-8
#
# Copyright 2011-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.
"""
manifestutil

Created by Greg Neagle on 2011-03-04.
"""

# TODO: add support for delete-manifest

import fnmatch
import optparse
import os
import plistlib
import readline
import shlex
import sys

from xml.parsers.expat import ExpatError

# our libs
from munkilib.cliutils import ConfigurationSaveError
from munkilib.cliutils import configure as _configure
from munkilib.cliutils import libedit
from munkilib.cliutils import get_version, pref, path2url

from munkilib import munkirepo


def get_installer_item_names(repo, catalog_limit_list):
    '''Returns a list of unique installer item (pkg) names
    from the given list of catalogs'''
    item_list = []
    try:
        catalogs_list = repo.itemlist('catalogs')
    except munkirepo.RepoError, err:
        print >> sys.stderr, (
            'Could not retreive catalogs: %s' % unicode(err))
        return []
    for catalog_name in catalogs_list:
        if catalog_name in catalog_limit_list:
            try:
                data = repo.get(os.path.join('catalogs', catalog_name))
                catalog = plistlib.readPlistFromString(data)
            except munkirepo.RepoError, err:
                print >> sys.stderr, (
                    'Could not retreive catalog %s: %s'
                    % (catalog_name, unicode(err)))
            except (IOError, OSError, ExpatError):
                # skip items that aren't valid plists
                # or that we can't read
                pass
            else:
                item_list.extend([item['name']
                                  for item in catalog
                                  if not item.get('update_for')])
    item_list = list(set(item_list))
    item_list.sort()
    return item_list


def get_manifest_names(repo):
    '''Returns a list of available manifests'''
    try:
        manifest_names = repo.itemlist('manifests')
    except munkirepo.RepoError, err:
        print >> sys.stderr, (
            'Could not retreive manifests: %s' % unicode(err))
        manifest_names = []
    manifest_names.sort()
    return manifest_names


def get_catalogs(repo):
    '''Returns a list of available catalogs'''
    try:
        catalog_names = repo.itemlist('catalogs')
    except munkirepo.RepoError, err:
        print >> sys.stderr, (
            'Could not retreive catalogs: %s' % unicode(err))
        catalog_names = []
    catalog_names.sort()
    return catalog_names


def get_manifest_pkg_sections():
    '''Returns a list of manifest sections that can contain pkg names'''
    return ['managed_installs',
            'managed_uninstalls',
            'managed_updates',
            'optional_installs']


def printplistitem(label, value, indent=0):
    """Prints a plist item in an 'attractive' way"""
    indentspace = '    '
    if value is None:
        print indentspace*indent, '%s: !NONE!' % label
    elif isinstance(value, list) or type(value).__name__ == 'NSCFArray':
        if label:
            print indentspace*indent, '%s:' % label
        for item in value:
            printplistitem('', item, indent+1)
    elif isinstance(value, dict) or type(value).__name__ == 'NSCFDictionary':
        if label:
            print indentspace*indent, '%s:' % label
        keys = list(value.keys())
        keys.sort()
        for subkey in keys:
            printplistitem(subkey, value[subkey], indent+1)
    else:
        if label:
            print indentspace*indent, '%s: %s' % (label, value)
        else:
            print indentspace*indent, '%s' % value


def printplist(plistdict):
    """Prints plist dictionary in a pretty(?) way"""
    keys = list(plistdict.keys())
    keys.sort()
    for key in keys:
        printplistitem(key, plistdict[key])


def get_manifest(repo, manifest_name):
    '''Gets the contents of a manifest'''
    manifest_ref = os.path.join('manifests', manifest_name)
    try:
        data = repo.get(manifest_ref)
        return plistlib.readPlistFromString(data)
    except munkirepo.RepoError, err:
        print >> sys.stderr, (u'Could not retreive manifest %s: %s'
                              % (manifest_name, unicode(err)))
        return None
    except (IOError, OSError, ExpatError), err:
        print >> sys.stderr, (
            u'Could not read manifest %s: %s' % (manifest_name, unicode(err)))
        return None


def save_manifest(repo, manifest_dict, manifest_name, overwrite_existing=False):
    '''Saves a manifest to disk'''
    existing_manifest_names = get_manifest_names(repo)
    if not overwrite_existing:
        if manifest_name in existing_manifest_names:
            print >> sys.stderr, '%s already exists!' % manifest_name
            return False
    manifest_ref = os.path.join('manifests', manifest_name)
    try:
        data = plistlib.writePlistToString(manifest_dict)
        repo.put(manifest_ref, data)
        return True
    except (IOError, OSError, ExpatError, munkirepo.RepoError), err:
        print >> sys.stderr, (
            u'Saving %s failed: %s' % (manifest_name, unicode(err)))
        return False


def manifest_rename(repo, source_manifest_name, dest_manifest_name,
                    overwrite_existing=False):
    '''Renames an existing manifest. Since the repo can be an API-based repo,
    we really have to save a copy under the new name and then delete the old
    one.'''
    source_manifest_ref = os.path.join('manifests', source_manifest_name)
    dest_manifest_ref = os.path.join('manifests', dest_manifest_name)
    existing_manifest_names = get_manifest_names(repo)
    if not overwrite_existing:
        if dest_manifest_name in existing_manifest_names:
            print >> sys.stderr, u'%s already exists!' % dest_manifest_name
            return False
    try:
        source_data = repo.get(source_manifest_ref)
        repo.put(dest_manifest_ref, source_data)
        repo.delete(source_manifest_ref)
        return True
    except munkirepo.RepoError, err:
        print >> sys.stderr, u'Renaming %s to %s failed: %s' % (
            source_manifest_name, dest_manifest_name, unicode(err))
        return False


def cleanup_and_exit(exitcode):
    """Give the user the chance to unmount the repo when we exit"""
    result = 0
    # TODO: handle mounted repo
    #if repo and repo.mounted and repo.WE_MOUNTED_THE_REPO:
    #    answer = raw_input('Unmount the repo fileshare? [y/n] ')
    #    if answer.lower().startswith('y'):
    #        result = repo.unmount()
    exit(exitcode or result)


def update_cached_manifest_list(repo):
    '''Updates our cached list of available manifests so our completer
    will return all the available manifests.'''
    CMD_ARG_DICT['manifests'] = get_manifest_names(repo)


##### subcommand functions #####

class MyOptParseError(Exception):
    '''Exception for our custom option parser'''
    pass


class MyOptParseExit(Exception):
    '''Raised when optparse calls self.exit() so we can handle it instead
    of exiting'''
    pass


class MyOptionParser(optparse.OptionParser):
    '''Custom option parser that overrides the error handler and
    the exit handler so printing help doesn't exit the interactive
    psuedo-shell'''
    def error(self, msg):
        """error(msg : string)

        """
        self.print_usage(sys.stderr)
        raise MyOptParseError('option error: %s' % msg)

    def exit(self, status=0, msg=None):
        if msg:
            sys.stderr.write(msg)
        raise MyOptParseExit


def version(repo, args):
    '''Prints version number'''
    # we ignore repo arg but subcommand dispatcher sends it to all
    # subcommands
    if len(args) != 0:
        print >> sys.stderr, 'Usage: version'
        return 22 # Invalid argument
    print get_version()
    return 0


def configure(repo, args):
    '''Allow user to configure tool options'''
    # we ignore the repo arg, but all the other subcommands require it
    if len(args):
        print >> sys.stderr, 'Usage: configure'
        return 22 # Invalid argument
    prompt_list = [
        ('repo_url', 'Repo URL (example: afp://munki.example.com/repo)'),
        ('plugin', 'Munki repo plugin (defaults to FileRepo)')
    ]
    try:
        _configure(prompt_list)
        return 0
    except ConfigurationSaveError:
        return 1 # Operation not permitted


def list_catalogs(repo, args):
    '''Prints the names of the available catalogs'''
    parser = MyOptionParser()
    parser.set_usage('''list-catalogs
       Prints the names of the available catalogs''')
    try:
        _, arguments = parser.parse_args(args)
    except MyOptParseError, errmsg:
        print >> sys.stderr, str(errmsg)
        return 22 # Invalid argument
    except MyOptParseExit:
        return 0

    if len(arguments) != 0:
        parser.print_usage(sys.stderr)
        return 22 # Invalid argument
    for item in get_catalogs(repo):
        print item
    return 0


def list_catalog_items(repo, args):
    '''Lists items in the given catalogs'''
    parser = MyOptionParser()
    parser.set_usage('''list-catalog-items CATALOG_NAME [CATALOG_NAME ...]
       Lists items in the given catalogs''')
    try:
        _, arguments = parser.parse_args(args)
    except MyOptParseError, errmsg:
        print >> sys.stderr, str(errmsg)
        return 22 # Invalid argument
    except MyOptParseExit:
        return 0

    if len(arguments) == 0:
        parser.print_usage(sys.stderr)
        return 1 # Operation not permitted
    available_catalogs = get_catalogs(repo)
    for catalog in arguments:
        if catalog not in available_catalogs:
            print >> sys.stderr, '%s: no such catalog!' % catalog
            return 2 # No such file or directory
    for pkg in get_installer_item_names(repo, arguments):
        print pkg
    return 0


def list_manifests(repo, args):
    '''Prints names of available manifests, filtering on arg[0]
    similar to Unix file globbing'''
    parser = MyOptionParser()
    parser.set_usage('''list-manifests [FILE_NAME_MATCH_STRING]
       Prints names of available manifests.''')
    try:
        _, arguments = parser.parse_args(args)
    except MyOptParseError, errmsg:
        print >> sys.stderr, str(errmsg)
        return 22 # Invalid argument
    except MyOptParseExit:
        return 0

    if len(arguments) == 1:
        list_filter = arguments[0]
    elif len(arguments) == 0:
        list_filter = '*'
    else:
        parser.print_usage(sys.stderr)
        return 7 # Argument list too long
    for item in get_manifest_names(repo):
        if fnmatch.fnmatch(item, list_filter):
            print item
    return 0


def find(repo, args):
    '''Find text in manifests, optionally searching just a specific manifest
    section specified by keyname'''
    parser = MyOptionParser()
    parser.set_usage('''find FIND_TEXT [--section SECTION_NAME]
       Find text in manifests, optionally searching a specific manifest
       section''')
    parser.add_option('--section',
                      metavar='SECTION_NAME',
                      help=('(Optional) Section of the manifest to search for '
                            'FIND_TEXT'))
    try:
        options, arguments = parser.parse_args(args)
    except MyOptParseError, errmsg:
        print >> sys.stderr, str(errmsg)
        return 22 # Invalid argument
    except MyOptParseExit:
        return 0

    if not options.section and len(arguments) == 2:
        options.section = arguments[1]
        del arguments[1]
    if len(arguments) != 1:
        parser.print_usage(sys.stderr)
        return 7 # Argument list too long
    findtext = arguments[0]
    keyname = options.section

    count = 0
    for name in get_manifest_names(repo):
        manifest_ref = os.path.join('manifests', name)
        try:
            data = repo.get(manifest_ref)
            manifest = plistlib.readPlistFromString(data)
        except (IOError, OSError, ExpatError, munkirepo.RepoError), err:
            print >> sys.stderr, (
                u'Error reading %s: %s' % (manifest_ref, unicode(err)))
            continue
        if keyname:
            if keyname in manifest:
                value = manifest[keyname]
                if type(value) == list or type(value).__name__ == 'NSCFArray':
                    for item in value:
                        try:
                            if findtext.upper() in item.upper():
                                print '%s: %s' % (name, item)
                                count += 1
                        except AttributeError:
                            pass
                elif findtext.upper() in value.upper():
                    print '%s: %s' % (name, value)
                    count += 1
        else:
            for key in manifest.keys():
                value = manifest[key]
                if type(value) == list or type(value).__name__ == 'NSCFArray':
                    for item in value:
                        try:
                            if findtext.upper() in item.upper():
                                print '%s (%s): %s' % (name, key, item)
                                count += 1
                        except AttributeError:
                            pass
                elif findtext.upper() in value.upper():
                    print '%s (%s): %s' % (name, key, value)
                    count += 1

    print '%s matches found.' % count
    return 0


def display_manifest(repo, args):
    '''Prints contents of a given manifest'''
    parser = MyOptionParser()
    parser.set_usage('''display-manifest MANIFESTNAME
       Prints the contents of the specified manifest''')
    try:
        _, arguments = parser.parse_args(args)
    except MyOptParseError, errmsg:
        print >> sys.stderr, str(errmsg)
        return 22 # Invalid argument
    except MyOptParseExit:
        return 0

    if len(arguments) != 1:
        parser.print_usage(sys.stderr)
        return 7 # Argument list too long
    manifestname = arguments[0]
    manifest = get_manifest(repo, manifestname)
    if manifest:
        printplist(manifest)
        return 0
    else:
        return 2 # No such file or directory


def expand_included_manifests(repo, args):
    '''Prints a manifest, expanding any included manifests.'''

    def manifest_recurser(repo, manifest):
        '''Recursive expansion of included manifests'''
        # No infinite loop checking! Be wary!
        if 'included_manifests' in manifest:
            for (index, item) in enumerate(manifest['included_manifests']):
                included_manifest = get_manifest(repo, item)
                if included_manifest:
                    included_manifest = manifest_recurser(repo, included_manifest)
                manifest['included_manifests'][index] = {
                    item: included_manifest
                }
        return manifest

    parser = MyOptionParser()
    parser.set_usage('''expand-included-manifest MANIFESTNAME
        Prints included manifests in the specified manifest''')
    try:
        _, arguments = parser.parse_args(args)
    except MyOptParseError, errmsg:
        print >> sys.stderr, str(errmsg)
        return 22 # Invalid argument
    except MyOptParseExit:
        return 0

    if len(arguments) != 1:
        parser.print_usage(sys.stderr)
        return 7 # Argument list too long
    manifestname = arguments[0]
    manifest = get_manifest(repo, manifestname)
    if manifest:
        printplist(manifest_recurser(repo, manifest))
    else:
        return 2 # No such file or directory


def new_manifest(repo, args):
    '''Creates a new, empty manifest'''
    parser = MyOptionParser()
    parser.set_usage('''new-manifest MANIFESTNAME
       Creates a new empty manifest''')
    try:
        _, arguments = parser.parse_args(args)
    except MyOptParseError, errmsg:
        print >> sys.stderr, str(errmsg)
        return 22 # Invalid argument
    except MyOptParseExit:
        return 0

    if len(arguments) != 1:
        parser.print_usage(sys.stderr)
        return 7 # Argument list too long
    manifest_name = arguments[0]
    manifest = {'catalogs': [],
                'included_manifests': [],
                'managed_installs': [],
                'managed_uninstalls': []}
    if save_manifest(repo, manifest, manifest_name):
        update_cached_manifest_list(repo)
        return 0
    else:
        return 1 # Operation not permitted


def copy_manifest(repo, args):
    '''Copies one manifest to another'''
    parser = MyOptionParser()
    parser.set_usage(
        '''copy-manifest SOURCE_MANIFEST DESTINATION_MANIFEST
       Copies the contents of one manifest to another''')
    try:
        _, arguments = parser.parse_args(args)
    except MyOptParseError, errmsg:
        print >> sys.stderr, str(errmsg)
        return 22 # Invalid argument
    except MyOptParseExit:
        return 0

    if len(arguments) != 2:
        parser.print_usage(sys.stderr)
        return 7 # Argument list too long
    source_manifest = arguments[0]
    dest_manifest = arguments[1]
    manifest = get_manifest(repo, source_manifest)
    if manifest and save_manifest(repo, manifest, dest_manifest):
        update_cached_manifest_list(repo)
        return 0
    else:
        return 1 # Operation not permitted


def rename_manifest(repo, args):
    '''Renames a manifest'''
    parser = MyOptionParser()
    parser.set_usage(
        '''rename-manifest SOURCE_MANIFEST DESTINATION_MANIFEST
       Renames the manifest''')
    try:
        _, arguments = parser.parse_args(args)
    except MyOptParseError, errmsg:
        print >> sys.stderr, str(errmsg)
        return 22 # Invalid argument
    except MyOptParseExit:
        return 0

    if len(arguments) != 2:
        parser.print_usage(sys.stderr)
        return 7 # Argument list too long
    source_manifest = arguments[0]
    dest_manifest = arguments[1]
    if manifest_rename(repo, source_manifest, dest_manifest):
        print ('Renamed manifest %s to %s.'
               % (source_manifest, dest_manifest))
        update_cached_manifest_list(repo)
        return 0
    else:
        return 1 # Operation not permitted


def add_pkg(repo, args):
    '''Adds a package to a manifest.'''
    parser = MyOptionParser()
    parser.set_usage(
        '''add-pkg PKGNAME --manifest MANIFESTNAME [--section SECTIONNAME]
       Adds a package to a manifest. Package is added to managed_installs
       unless a different manifest section is specified with the
       --section option''')
    parser.add_option('--manifest',
                      metavar='MANIFESTNAME',
                      help='name of manifest on which to operate')
    parser.add_option('--section', default='managed_installs',
                      metavar='SECTIONNAME',
                      help=('manifest section to which to add the package. '
                            'Defaults to managed_installs.'))
    try:
        options, arguments = parser.parse_args(args)
    except MyOptParseError, errmsg:
        print >> sys.stderr, str(errmsg)
        return 22 # Invalid argument
    except MyOptParseExit:
        return 0

    if not options.manifest:
        if len(arguments) == 2:
            options.manifest = arguments[1]
            del arguments[1]
        else:
            parser.print_usage(sys.stderr)
            return 7 # Argument list too long
    if len(arguments) != 1:
        parser.print_usage(sys.stderr)
        return 7 # Argument list too long
    pkgname = arguments[0]

    manifest = get_manifest(repo, options.manifest)
    if not manifest:
        return 2 # No such file or directory
    for section in get_manifest_pkg_sections():
        if pkgname in manifest.get(section, []):
            print >> sys.stderr, (
                'Package %s is already in section %s of manifest %s.'
                % (pkgname, section, options.manifest))
            return 1 # Operation not permitted

    manifest_catalogs = manifest.get('catalogs', [])
    available_pkgnames = get_installer_item_names(repo, manifest_catalogs)
    if pkgname not in available_pkgnames:
        print >> sys.stderr, (
            'WARNING: Package %s is not available in catalogs %s '
            'of manifest %s.'
            % (pkgname, manifest_catalogs, options.manifest))
    if not options.section in manifest:
        manifest[options.section] = [pkgname]
    else:
        manifest[options.section].append(pkgname)
    if save_manifest(repo, manifest, options.manifest, overwrite_existing=True):
        print ('Added %s to section %s of manifest %s.'
               % (pkgname, options.section, options.manifest))
        return 0
    else:
        return 1 # Operation not permitted


def remove_pkg(repo, args):
    '''Removes a package from a manifest.'''
    parser = MyOptionParser()
    parser.set_usage(
        '''remove-pkg PKGNAME --manifest MANIFESTNAME [--section SECTIONNAME]
       Removes a package from a manifest. Package is removed from
       managed_installs unless a different manifest section is specified with
       the --section option''')
    parser.add_option('--manifest',
                      metavar='MANIFESTNAME',
                      help='''name of manifest on which to operate''')
    parser.add_option('--section', default='managed_installs',
                      metavar='SECTIONNAME',
                      help=('manifest section from which to remove the '
                            'package. Defaults to managed_installs.'))
    try:
        options, arguments = parser.parse_args(args)
    except MyOptParseError, errmsg:
        print >> sys.stderr, str(errmsg)
        return 22 # Invalid argument
    except MyOptParseExit:
        return 0

    if not options.manifest:
        if len(arguments) == 2:
            options.manifest = arguments[1]
            del arguments[1]
        else:
            parser.print_usage(sys.stderr)
            return 7 # Argument list too long
    if len(arguments) != 1:
        parser.print_usage(sys.stderr)
        return 7 # Argument list too long
    pkgname = arguments[0]

    manifest = get_manifest(repo, options.manifest)
    if not manifest:
        return 2 # No such file or directory
    if not options.section in manifest:
        print >> sys.stderr, ('Section %s is not in manifest %s.'
                              % (options.section, manifest))
        return 1 # Operation not permitted
    if pkgname not in manifest[options.section]:
        print >> sys.stderr, ('Package %s is not in section %s '
                              'of manifest %s.'
                              % (pkgname, options.section, options.manifest))
        return 1 # Operation not permitted
    else:
        manifest[options.section].remove(pkgname)
        if save_manifest(repo, manifest, options.manifest,
                         overwrite_existing=True):
            print ('Removed %s from section %s of manifest %s.'
                   % (pkgname, options.section, options.manifest))
            return 0
        else:
            return 1 # Operation not permitted


def add_catalog(repo, args):
    '''Adds a catalog to a manifest.'''
    parser = MyOptionParser()
    parser.set_usage('''add-catalog CATALOGNAME --manifest MANIFESTNAME
       Adds a catalog to a manifest''')
    parser.add_option('--manifest',
                      metavar='MANIFESTNAME',
                      help='name of manifest on which to operate')
    try:
        options, arguments = parser.parse_args(args)
    except MyOptParseError, errmsg:
        print >> sys.stderr, str(errmsg)
        return 22 # Invalid argument
    except MyOptParseExit:
        return 0

    if not options.manifest:
        if len(arguments) == 2:
            options.manifest = arguments[1]
            del arguments[1]
        else:
            parser.print_usage(sys.stderr)
            return 7 # Argument list too long
    if len(arguments) != 1:
        parser.print_usage(sys.stderr)
        return 7 # Argument list too long
    catalogname = arguments[0]

    available_catalogs = get_catalogs(repo)
    if catalogname not in available_catalogs:
        print >> sys.stderr, 'Unknown catalog name: %s.' % catalogname
        return 2 # no such file or directory

    manifest = get_manifest(repo, options.manifest)
    if not manifest:
        return 2 # no such file or directory
    if not 'catalogs' in manifest:
        manifest['catalogs'] = []
    if catalogname in manifest['catalogs']:
        print >> sys.stderr, (
            'Catalog %s is already in manifest %s.'
            % (catalogname, options.manifest))
        return 1 # Operation not permitted
    else:
        # put it at the front of the catalog list as that is usually
        # what is wanted...
        manifest['catalogs'].insert(0, catalogname)
        if save_manifest(repo, manifest, options.manifest,
                         overwrite_existing=True):
            print ('Added %s to catalogs of manifest %s.'
                   % (catalogname, options.manifest))
            return 0
        else:
            return 1 # Operation not permitted


def remove_catalog(repo, args):
    '''Removes a catalog from a manifest.'''
    parser = MyOptionParser()
    parser.set_usage('''remove-catalog CATALOGNAME --manifest MANIFESTNAME
       Removes a catalog from a manifest''')
    parser.add_option('--manifest',
                      metavar='MANIFESTNAME',
                      help='name of manifest on which to operate')
    try:
        options, arguments = parser.parse_args(args)
    except MyOptParseError, errmsg:
        print >> sys.stderr, str(errmsg)
        return 22 # Invalid argument
    except MyOptParseExit:
        return 0

    if not options.manifest:
        if len(arguments) == 2:
            options.manifest = arguments[1]
            del arguments[1]
        else:
            parser.print_usage(sys.stderr)
            return 7 # Argument list too long
    if len(arguments) != 1:
        parser.print_usage(sys.stderr)
        return 7 # Argument list too long
    catalogname = arguments[0]

    manifest = get_manifest(repo, options.manifest)
    if not manifest:
        return 2 # no such file or directory
    if catalogname not in manifest.get('catalogs', []):
        print >> sys.stderr, (
            'Catalog %s is not in manifest %s.'
            % (catalogname, options.manifest))
        return 1 # Operation not permitted
    else:
        manifest['catalogs'].remove(catalogname)
        if save_manifest(repo, manifest, options.manifest,
                         overwrite_existing=True):
            print ('Removed %s from catalogs of manifest %s.'
                   % (catalogname, options.manifest))
            return 0
        else:
            return 1 # Operation not permitted


def add_included_manifest(repo, args):
    '''Adds an included manifest to a manifest.'''
    parser = MyOptionParser()
    parser.set_usage(
        '''add-included-manifest MANIFEST_TO_INCLUDE --manifest TARGET_MANIFEST
       Adds a manifest to the included_manifests of the TARGET_MANIFEST''')
    parser.add_option('--manifest',
                      metavar='TARGET_MANIFEST',
                      help='name of manifest on which to operate')
    try:
        options, arguments = parser.parse_args(args)
    except MyOptParseError, errmsg:
        print >> sys.stderr, str(errmsg)
        return 22 # Invalid argument
    except MyOptParseExit:
        return 0

    if not options.manifest:
        if len(arguments) == 2:
            options.manifest = arguments[1]
            del arguments[1]
        else:
            parser.print_usage(sys.stderr)
            return 7 # Argument list too long
    if len(arguments) != 1:
        parser.print_usage(sys.stderr)
        return 7 # Argument list too long
    manifest_to_include = arguments[0]

    available_manifests = get_manifest_names(repo)
    if manifest_to_include not in available_manifests:
        print >> sys.stderr, ('Unknown manifest name: %s.'
                              % manifest_to_include)
        return 2 # no such file or directory
    if manifest_to_include == options.manifest:
        print >> sys.stderr, ('Can\'t include %s in itself!.'
                              % manifest_to_include)
        return 1 # Operation not permitted

    manifest = get_manifest(repo, options.manifest)
    if not manifest:
        return 2 # no such file or directory
    if not 'included_manifests' in manifest:
        manifest['included_manifests'] = []
    if manifest_to_include in manifest['included_manifests']:
        print >> sys.stderr, (
            'Manifest %s is already included in manifest %s.'
            % (manifest_to_include, options.manifest))
        return 1 # Operation not permitted
    else:
        manifest['included_manifests'].append(manifest_to_include)
        if save_manifest(repo, manifest, options.manifest,
                         overwrite_existing=True):
            print ('Added %s to included_manifests of manifest %s.'
                   % (manifest_to_include, options.manifest))
            return 0
        else:
            return 1 # Operation not permitted


def remove_included_manifest(repo, args):
    '''Removes an included manifest from a manifest.'''
    parser = MyOptionParser()
    parser.set_usage(
        '''remove-included_manifest INCLUDED_MANIFEST --manifest TARGET_MANIFEST
       Removes a manifest from the included_manifests of TARGET_MANIFEST''')
    parser.add_option('--manifest',
                      metavar='TARGET_MANIFEST',
                      help='name of manifest on which to operate')
    try:
        options, arguments = parser.parse_args(args)
    except MyOptParseError, errmsg:
        print >> sys.stderr, str(errmsg)
        return 22 # Invalid argument
    except MyOptParseExit:
        return 0

    if not options.manifest:
        if len(arguments) == 2:
            options.manifest = arguments[1]
            del arguments[1]
        else:
            parser.print_usage(sys.stderr)
            return 7 # Argument list too long
    if len(arguments) != 1:
        parser.print_usage(sys.stderr)
        return 7 # Argument list too long
    included_manifest = arguments[0]

    manifest = get_manifest(repo, options.manifest)
    if not manifest:
        return 2 # no such file or directory
    if included_manifest not in manifest.get('included_manifests', []):
        print >> sys.stderr, (
            'Manifest %s is not included in manifest %s.'
            % (included_manifest, options.manifest))
        return 1 # Operation not permitted
    else:
        manifest['included_manifests'].remove(included_manifest)
        if save_manifest(repo, manifest, options.manifest,
                         overwrite_existing=True):
            print ('Removed %s from included_manifests of manifest %s.'
                   % (included_manifest, options.manifest))
            return 0
        else:
            return 1 # Operation not permitted


def refresh_cache(repo, args):
    '''Refreshes the repo data if changes were made while manifestutil was
    running. Updates manifests, catalogs, and packages.'''
    parser = MyOptionParser()
    parser.set_usage('''refresh-cache
        Refreshes the repo data''')
    try:
        _, arguments = parser.parse_args(args)
    except MyOptParseError, errmsg:
        print >> sys.stderr, str(errmsg)
        return 22 # Invalid argument
    except MyOptParseExit:
        return 0

    if len(arguments) != 0:
        parser.print_usage(sys.stderr)
        return 22 # Invalid argument
    CMD_ARG_DICT['manifests'] = get_manifest_names(repo)
    CMD_ARG_DICT['catalogs'] = get_catalogs(repo)
    CMD_ARG_DICT['pkgs'] = get_installer_item_names(
        repo, CMD_ARG_DICT['catalogs'])

##### end subcommand functions

def show_help():
    '''Prints available subcommands'''
    print "Available sub-commands:"
    subcommands = CMD_ARG_DICT['cmds'].keys()
    subcommands.sort()
    for item in subcommands:
        print '\t%s' % item
    return 0


def tab_completer(text, state):
    '''Called by the readline lib to calculate possible completions'''
    array_to_match = None
    if readline.get_begidx() == 0:
        # since we are at the start of the line
        # we are matching commands
        array_to_match = 'cmds'
        match_list = CMD_ARG_DICT.get('cmds', {}).keys()
    else:
        # we are matching args
        cmd_line = readline.get_line_buffer()[0:readline.get_begidx()]
        cmd = shlex.split(cmd_line)[-1]
        array_to_match = CMD_ARG_DICT.get('cmds', {}).get(cmd)
        if array_to_match:
            match_list = CMD_ARG_DICT[array_to_match]
        else:
            array_to_match = CMD_ARG_DICT.get('options', {}).get(cmd)
            if array_to_match:
                match_list = CMD_ARG_DICT[array_to_match]
            else:
                array_to_match = 'options'
                match_list = CMD_ARG_DICT.get('options', {}).keys()

    matches = [item for item in match_list
               if item.upper().startswith(text.upper())]
    try:
        return matches[state]
    except IndexError:
        return None


def set_up_tab_completer():
    '''Starts our tab-completer when running interactively'''
    readline.set_completer(tab_completer)
    if libedit:
        # readline module was compiled against libedit
        readline.parse_and_bind("bind ^I rl_complete")
    else:
        readline.parse_and_bind("tab: complete")


def handle_subcommand(repo, args):
    '''Does all our subcommands'''
    # check if any arguments are passed.
    # if not, list subcommands.
    if len(args) < 1:
        show_help()
        return 2

    # strip leading hyphens and
    # replace embedded hyphens with underscores
    # so '--add-pkg' becomes 'add_pkg'
    # and 'new-manifest' becomes 'new_manifest'
    subcommand = args[0].lstrip('-').replace('-', '_')

    # special case the exit command
    if subcommand == 'exit':
        cleanup_and_exit(0)

    if (subcommand not in ['version', 'configure', 'help']
            and '-h' not in args and '--help' not in args):
        if not repo:
            repo = connect_to_repo()

    try:
        # find function to call by looking in the global name table
        # for a function with a name matching the subcommand
        subcommand_function = globals()[subcommand]
    except MyOptParseExit:
        return 0
    except (TypeError, KeyError):
        print >> sys.stderr, 'Unknown subcommand: %s' % args[0]
        show_help()
        return 2
    else:
        return subcommand_function(repo, args[1:])


def connect_to_repo():
    '''Connects to the Munki repo'''
    repo_url = pref('repo_url')
    repo_path = pref('repo_path')
    repo_plugin = pref('plugin')
    if not repo_url and repo_path:
        repo_url = path2url(repo_path)
    if not repo_url:
        print >> sys.stderr, (
            u'No repo URL defined. Run %s --configure to define one.'
            % os.path.basename(__file__))
        exit(-1)
    try:
        repo = munkirepo.connect(repo_url, repo_plugin)
    except munkirepo.RepoError, err:
        print >> sys.stderr, u'Repo error: %s' % unicode(err)
        exit(-1)
    return repo


CMD_ARG_DICT = {}

def main():
    '''Our main routine'''
    repo = None

    cmds = {'add-pkg':                   'pkgs',
            'add-catalog':               'catalogs',
            'add-included-manifest':     'manifests',
            'remove-pkg':                'pkgs',
            'remove-catalog':            'catalogs',
            'remove-included-manifest':  'manifests',
            'list-manifests':            'manifests',
            'list-catalogs':             'default',
            'list-catalog-items':        'catalogs',
            'display-manifest':          'manifests',
            'expand-included-manifests': 'manifests',
            'find':                      'default',
            'new-manifest':              'default',
            'copy-manifest':             'manifests',
            'rename-manifest':           'manifests',
            'refresh-cache':             'default',
            'exit':                      'default',
            'help':                      'default',
            'configure':                 'default',
            'version':                   'default'
           }
    CMD_ARG_DICT['cmds'] = cmds

    if len(sys.argv) > 1:
        # some commands or options were passed at the command line
        cmd = sys.argv[1].lstrip('-')
        retcode = handle_subcommand(repo, sys.argv[1:])
        cleanup_and_exit(retcode)
    else:
        # if we get here, no options or commands,
        # so let's enter interactive mode
        # must have an available repo for interactive mode
        repo = connect_to_repo()
        # build the rest of our dict to enable tab completion
        CMD_ARG_DICT['options'] = {'--manifest': 'manifests',
                                   '--section':  'sections'}

        CMD_ARG_DICT['default'] = []
        CMD_ARG_DICT['sections'] = get_manifest_pkg_sections()
        CMD_ARG_DICT['manifests'] = get_manifest_names(repo)
        CMD_ARG_DICT['catalogs'] = get_catalogs(repo)
        CMD_ARG_DICT['pkgs'] = get_installer_item_names(
            repo, CMD_ARG_DICT['catalogs'])

        set_up_tab_completer()
        print 'Entering interactive mode... (type "help" for commands)'
        while 1:
            try:
                cmd = raw_input('> ')
            except (KeyboardInterrupt, EOFError):
                # React to Control-C and Control-D
                print # so we finish off the raw_input line
                cleanup_and_exit(0)
            args = shlex.split(cmd)
            handle_subcommand(repo, args)

if __name__ == '__main__':
    main()
