from ConfigParser import ConfigParser
from collections import defaultdict
import copy
import json
import logging
import os.path
import re
import sys

# This must be called early - before the rest of the blueprint library loads.
logging.basicConfig(format='# [blueprint] %(message)s',
                    level=logging.INFO)

import git
import rules
import util
import walk


DEFAULTS = {'io': {'max_content_length': 67108864,
                   'server': 'https://devstructure.com'},
            's3': {'region': 'US',
                   'use_https': True},
            'statsd': {'port': 8125}}


cfg = ConfigParser()
for section, options in DEFAULTS.iteritems():
    cfg.add_section(section)
    for option, value in options.iteritems():
        cfg.set(section, option, str(value))
cfg.read(['/etc/blueprint.cfg',
          os.path.expanduser('~/.blueprint.cfg')])


class NameError(ValueError):
    pass


class NotFoundError(KeyError):
    pass


class Blueprint(dict):

    DISCLAIMER = """#
# Automatically generated by blueprint(7).  Edit at your own risk.
#
"""

    @classmethod
    def checkout(cls, name, commit=None):
        git.init()
        if commit is None:
            commit = git.rev_parse('refs/heads/{0}'.format(name))
            if commit is None:
                raise NotFoundError(name)
        tree = git.tree(commit)
        blob = git.blob(tree, 'blueprint.json')
        content = git.content(blob)
        return cls(name, commit, **json.loads(content))

    @classmethod
    def create(cls, name):
        b = cls(name)
        r = rules.defaults()
        import backend
        for funcname in backend.__all__:
            getattr(backend, funcname)(b, r)
        import services
        services.services(b)
        return b

    @classmethod
    def destroy(cls, name):
        """
        Destroy the named blueprint.
        """
        if not os.path.isdir(git.repo()):
            raise NotFoundError(name)
        try:
            git.git('branch', '-D', name)
        except:
            raise NotFoundError(name)

    @classmethod
    def iter(cls):
        """
        Yield the name of each blueprint.
        """
        if not os.path.isdir(git.repo()):
            return
        status, stdout = git.git('branch')
        for line in stdout.splitlines():
            yield line.strip()

    @classmethod
    def load(cls, f, name=None):
        """
        Instantiate and return a Blueprint object from a file-like object
        from which valid blueprint JSON may be read.
        """
        return cls(name, **json.load(f))

    @classmethod
    def loads(cls, s, name=None):
        """
        Instantiate and return a Blueprint object from a string containing
        valid blueprint JSON.
        """
        return cls(name, **json.loads(s))

    @classmethod
    def rules(cls, r, name=None):
        b = cls(name)
        import backend
        for funcname in backend.__all__:
            getattr(backend, funcname)(b, r)
        import services
        services.services(b)
        return b

    def __init__(self, name=None, commit=None, *args, **kwargs):
        """
        Construct a blueprint.  Extra arguments are used to create a `dict`
        which is then sent through the `blueprint`(5) algorithm to be injested
        into this `Blueprint` object with the proper types.  (The structure
        makes heavy use of `defaultdict` and `set`).
        """
        self.name = name
        self._commit = commit
        def file(pathname, f):
            self.add_file(pathname, **f)
        def package(manager, package, version):
            self.add_package(manager, package, version)
        def service(manager, service):
            self.add_service(manager, service)
        def service_file(manager, service, pathname):
            self.add_service_file(manager, service, pathname)
        def service_package(manager, service, package_manager, package):
            self.add_service_package(manager,
                                     service,
                                     package_manager,
                                     package)
        def service_source(manager, service, dirname):
            self.add_service_source(manager, service, dirname)
        def source(dirname, filename, gen_content, url):
            if url is not None:
                self.add_source(dirname, url)
            elif gen_content is not None:
                self.add_source(dirname, filename)
        walk.walk(dict(*args, **kwargs),
                  file=file,
                  package=package,
                  service=service,
                  service_file=service_file,
                  service_package=service_package,
                  service_source=service_source,
                  source=source)

    def __sub__(self, other):
        """
        Subtracting one blueprint from another allows blueprints to remain
        free of superfluous packages from the base installation.  It takes
        three passes through the package tree.  The first two remove
        superfluous packages and the final one accounts for some special
        dependencies by adding them back to the tree.
        """
        b = copy.deepcopy(self)

        # Compare file contents and metadata.  Keep files that differ.
        for pathname, file in self.files.iteritems():
            if other.files.get(pathname, {}) == file:
                del b.files[pathname]

        # The first pass removes all duplicate packages that are not
        # themselves managers.  Allowing multiple versions of the same
        # packages complicates things slightly.  For each package, each
        # version that appears in the other blueprint is removed from
        # this blueprint.  After that is finished, this blueprint is
        # normalized.  If no versions remain, the package is removed.
        def package(manager, package, version):
            if package in b.packages:
                return
            try:
                b_packages = b.packages[manager]
            except KeyError:
                return
            if manager in b_packages:
                return
            if package not in b_packages:
                return
            try:
                b_versions = b_packages[package]
            except KeyError:
                return
            b_versions.discard(version)
            if 0 == len(b_versions):
                del b_packages[package]
            else:
                b_packages[package] = b_versions
        other.walk(package=package)

        # The second pass removes managers that manage no packages, a
        # potential side-effect of the first pass.  This step must be
        # applied repeatedly until the blueprint reaches a steady state.
        def package(manager, package, version):
            if package not in b.packages:
                return
            if 0 == len(b.packages[package]):
                del b.packages[package]
                del b.packages[self.managers[package]][package]
        while 1:
            l = len(b.packages)
            other.walk(package=package)
            if len(b.packages) == l:
                break

        # The third pass adds back special dependencies like `ruby*-dev`.
        # It isn't apparent from the rules above that a manager like RubyGems
        # needs more than just itself to function.  In some sense, this might
        # be considered a missing dependency in the Debian archive but in
        # reality it's only _likely_ that you need `ruby*-dev` to use
        # `rubygems*`.
        def after_packages(manager):
            if manager not in b.packages:
                return

            deps = {r'^python(\d+(?:\.\d+)?)$': ['python{0}',
                                                 'python{0}-dev',
                                                 'python',
                                                 'python-devel'],
                    r'^ruby(\d+\.\d+(?:\.\d+)?)$': ['ruby{0}-dev'],
                    r'^rubygems(\d+\.\d+(?:\.\d+)?)$': ['ruby{0}',
                                                        'ruby{0}-dev',
                                                        'ruby',
                                                        'ruby-devel']}

            for pattern, packages in deps.iteritems():
                match = re.search(pattern, manager)
                if match is None:
                    continue
                for package in packages:
                    package = package.format(match.group(1))
                    for managername in ('apt', 'yum'):
                        mine = self.packages.get(managername, {}).get(package,
                                                                      None)
                        if mine is not None:
                            b.packages[managername][package] = mine
        other.walk(after_packages=after_packages)

        # Compare service metadata.  Keep services that differ.
        for manager, services in self.services.iteritems():
            for service, deps in services.iteritems():
                if other.services.get(manager, {}).get(service, {}) == deps:
                    del b.services[manager][service]
            if 0 == len(b.services[manager]):