from collections import defaultdict, namedtuple
import glob
import gzip
import logging
import os
import shutil
import StringIO
import stat
import subprocess
import tempfile

import common
import opkg
import release as bismark_release

_NodePackage = namedtuple('NodePackage',
                          ['node', 'name', 'version', 'architecture'])


class NodePackage(_NodePackage):

    @property
    def package(self):
        return bismark_release.Package(self.name, self.version, self.architecture)


def deploy(releases_root,
           destination,
           signing_key,
           releases,
           experiments,
           node_groups):
    deployment_path = tempfile.mkdtemp(prefix='bismark-downloads-staging-')
    logging.info('staging deployment in %s', deployment_path)

    # Fix permissons of the deployment path. mkdtemp gives 700 permissions,
    # and rsync will copy those permissions to the Web server, so we end
    # up with permission denied errors unless we fix permissions here.
    user_perms = stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR
    group_perms = stat.S_IRGRP | stat.S_IXGRP
    other_perms = stat.S_IROTH | stat.S_IXOTH
    os.chmod(deployment_path, user_perms | group_perms | other_perms)

    for release in releases:
        _deploy_packages(release, deployment_path)
        _deploy_images(release, deployment_path)
        _deploy_builtin_packages(release, deployment_path)
        _deploy_extra_packages(release, deployment_path)
        _deploy_upgrades(release, node_groups, deployment_path)
        _deploy_experiment_packages(release,
                                    experiments,
                                    node_groups,
                                    deployment_path)
        _deploy_experiment_configurations(release,
                                          experiments,
                                          node_groups,
                                          deployment_path)
    _make_dummy_directories(deployment_path)
    _deploy_dummy_experiment_configurations(deployment_path)
    _deploy_packages_gz(deployment_path)
    _deploy_packages_sig(deployment_path, signing_key)
    _deploy_upgradable_sentinels(deployment_path)
    _deploy_static(releases_root, deployment_path)

    print 'The following files differ at the destination:'
    diff_success = _diff_from_destination(deployment_path, destination)

    if diff_success:
        deploy_response = raw_input('\nDeploy to %s? (y/N) ' % (destination,))
        if deploy_response == 'y':
            print 'Deploying from %s to %s' % (deployment_path, destination)
            _copy_to_destination(deployment_path, destination)
        else:
            print 'Skipping deployment'

    clean_response = raw_input(
        '\nDelete staging directory %s? (Y/n) ' % (deployment_path,))
    if clean_response != 'n':
        print 'Removing staging directory %s' % (deployment_path,)
        shutil.rmtree(deployment_path)
    else:
        print 'Staging directory %s left intact' % (deployment_path,)


def _deploy_packages(release, deployment_path):
    packages_path = release.packages_path
    for package in release.packages:
        destination = os.path.join(deployment_path,
                                   'packages',
                                   release.name,
                                   package.architecture)
        common.makedirs(destination)
        destination_path = os.path.join(destination, package.filename)
        source_filename = os.path.join(packages_path, '%s.ipk' % package.sha1)
        shutil.copy2(source_filename, destination_path)


def _deploy_images(release, deployment_path):
    images_path = release.images_path
    for image in release.images:
        destination_dir = os.path.join(deployment_path,
                                       release.name,
                                       image.architecture)
        common.makedirs(destination_dir)
        shutil.copy2(os.path.join(images_path, image.name), destination_dir)


def _deployment_package_paths(release, deployment_path):
    logging.info('locating package in deployed path')
    package_paths = dict()
    for package in release.packages:
        package_path = os.path.join(
            deployment_path,
            'packages',
            release.name,
            package.architecture,
            package.filename)
        package_paths[package.package] = package_path
    return package_paths


def _deploy_builtin_packages(release, deployment_path):
    package_paths = _deployment_package_paths(release, deployment_path)
    for package in release.builtin_packages:
        source = package_paths[package]
        architectures = release.normalize_architecture(package.architecture)
        for architecture in architectures:
            link_dir = os.path.join(deployment_path,
                                    release.name,
                                    architecture,
                                    'packages')
            common.makedirs(link_dir)
            link_name = os.path.join(link_dir, os.path.basename(source))
            relative_source = os.path.relpath(source, link_dir)
            os.symlink(relative_source, link_name)


def _deploy_extra_packages(release, deployment_path):
    package_paths = _deployment_package_paths(release, deployment_path)
    for package in release.extra_packages:
        source = package_paths[package]
        architectures = release.normalize_architecture(package.architecture)
        for architecture in architectures:
            link_dir = os.path.join(deployment_path,
                                    release.name,
                                    architecture,
                                    'extra-packages')
            common.makedirs(link_dir)
            link_name = os.path.join(link_dir, os.path.basename(source))
            relative_source = os.path.relpath(source, link_dir)
            os.symlink(relative_source, link_name)


def _resolve_groups_to_nodes(node_groups, group_packages):
    logging.info('resolving groups to nodes')
    node_packages = set()
    for group_package in group_packages:
        for node in node_groups.resolve_to_nodes(group_package.group):
            node_package = NodePackage(node,
                                       group_package.name,
                                       group_package.version,
                                       group_package.architecture)
            node_packages.add(node_package)

    # TODO(sburnett): Change this to pick the latest version instead of
    # throwing an error.
    packages_per_node = set()
    for package in node_packages:
        key = (package.node, package.name, package.architecture)
        if key in packages_per_node:
            raise Exception('Conflicting package versions for a node: %s' % (key,))
        packages_per_node.add(key)

    return node_packages


def _normalize_default_packages(node_packages, nodes):
    logging.info('normalizing packages')
    packages = defaultdict(dict)
    for node_package in node_packages:
        if node_package.node == 'default':
            continue
        key = (node_package.name, node_package.architecture)
        packages[key][node_package.node] = node_package.version
    for node_package in node_packages:
        if node_package.node != 'default':
            continue
        key = (node_package.name, node_package.architecture)
        for node in nodes:
            if node in packages[key]:
                continue
            packages[key][node] = node_package.version
    normalized_packages = set()
    for (name, architecture), nodes in packages.items():
        for node, version in nodes.items():
            node_package = NodePackage(node, name, version, architecture)
            normalized_packages.add(node_package)
    return normalized_packages


def _symlink_packages(release, packages, subdirectory, deployment_path):
    package_paths = _deployment_package_paths(release, deployment_path)
    for package in packages:
        source = package_paths[package.package]
        architectures = release.normalize_architecture(package.architecture)
        for architecture in architectures:
            link_dir = os.path.join(deployment_path,
                                    release.name,
                                    architecture,
                                    subdirectory,
                                    package.node)
            common.makedirs(link_dir)
            link_name = os.path.join(link_dir, os.path.basename(source))
            relative_source = os.path.relpath(source, link_dir)
            os.symlink(relative_source, link_name)


def _deploy_upgrades(release, node_groups, deployment_path):
    resolved_upgrades = _resolve_groups_to_nodes(
        node_groups,
        release.package_upgrades)
    nodes = set()
    for node_package in resolved_upgrades:
        nodes.add(node_package.node)
    upgraded_packages = _normalize_default_packages(resolved_upgrades, nodes)
    _symlink_packages(release,
                      upgraded_packages,
                      'updates-device',
                      deployment_path)


def _deploy_experiment_packages(release,
                                experiments,
                                node_groups,
                                deployment_path):