import os
import logging
import requests
from collections import OrderedDict
from substance.monads import *
from substance.logs import *
from substance.exceptions import *
from substance.shell import Shell
from substance.utils import (
    readYAML,
    writeYAML,
    readSupportFile,
    getSupportFile,
    streamDownload,
    makeXHRRequest,
    sha1sum,
    untar
)

logger = logging.getLogger(__name__)


class Box(object):

    def __init__(self, core, name, version, namespace, registry=None, boxstring=None, archiveSHA1=None):
        self.core = core
        self.name = name
        self.version = version
        self.namespace = namespace
        self.registry = registry
        self.boxstring = boxstring
        self.archiveSHA1 = archiveSHA1

    def getRegistryURL(self):
        return "https://%(registry)s/%(namespace)s/%(name)s/%(version)s.json" % self.__dict__

    def getArchiveURL(self):
        return "https://%(registry)s/%(namespace)s/%(name)s/%(version)s.box" % self.__dict__

    def getDescriptor(self):
        return {'name': self.name, 'registry': self.registry, 'namespace': self.namespace}

    def getPath(self):
        parts = [self.core.getBoxesPath(), self.registry, self.namespace,
                 self.name, self.version]
        return os.path.join(*parts)

    def getFilePath(self, filename):
        return os.path.join(self.getPath(), filename)

    def getArchivePath(self):
        return self.getFilePath("box.box")

    def getOVFFile(self):
        return self.getFilePath("box.ovf")

    def getImageName(self):
        return "%(name)s:%(version)s" % self.__dict__

    @staticmethod
    def fromBoxString(name):
        return Box.parseBoxString(name).map(lambda b: Box(**b))

    @staticmethod
    def parseBoxString(name):
        '''
        Parse a box string into dict of the components.

        Format: namespace/name:VERSION@registry
        registry and version are optional.
        '''

        box = OrderedDict()

        box['boxstring'] = name
        parts = name.split("@", 1)
        boxstring = parts[0]

        if len(parts) > 1:
            box['registry'] = parts[1]
        else:
            box['registry'] = "dl.bintray.com/turbulent/substance-images"

        parts = boxstring.split("/", 1)
        if len(parts) <= 1:
            return Fail(InvalidBoxName("Name \"%s\" does not reference a valid box name: Missing namespace." % name))

        (box['namespace'], imagestring) = parts
        # substance-box:0.1

        imageparts = imagestring.split(":", 1)
        if len(imageparts) > 1:
            (box['name'], box['version']) = imageparts
        else:
            box['name'] = imageparts[0]
            box['version'] = 'latest'

        return OK(box)

    def fetch(self):
        if self.isDownloaded():
            return OK(self)

        manifestURL = self.getRegistryURL()
        logger.debug("Downloading manifest: %s", manifestURL)
        return Try.attempt(makeXHRRequest, url=manifestURL) \
            .catch(lambda err: Fail(InvalidBoxName("Failed to fetch the box manifest for %s. Does this box exist?" % self.boxstring))) \
            .bind(defer(self.download))

    def isDownloaded(self):
        return True if os.path.exists(self.getOVFFile()) else False

    def getShortBoxString(self):
        return "%s/%s:%s" % (self.namespace, self.name, self.version)

    def delete(self):
        logger.info("Removing box %s from box cache", self.getShortBoxString())
        return Try.sequence([
            self.core.getDB().removeBoxRecord(self),
            Shell.nukeDirectory(self.getPath())
        ])

    def download(self, boxResult):
        archiveURL = boxResult['archiveURL']
        archiveSHA = boxResult['archiveSHA1']
        archive = self.getArchivePath()

        logger.info("Downloading %s:%s (%s)", self.name,
                    self.version, boxResult['archiveURL'])

        return Try.sequence([
            Shell.makeDirectory(os.path.dirname(archive)),
            Try.attempt(streamDownload, url=archiveURL, destination=archive)
            .then(defer(self.verifyArchive, expectedSHA=archiveSHA))
            .then(defer(self.unpackArchive)),
            self.core.getDB().updateBoxRecord(self, boxResult)
        ]).map(lambda x: self)

    def getArchiveHash(self):
        return sha1sum(self.getArchivePath())

    def verifyArchive(self, expectedSHA):
        logger.info("Verifying archive for %s", self.getImageName())
        sha = self.getArchiveHash()
        if sha == expectedSHA:
            return OK(self)
        else:
            Try.attempt(lambda: os.remove(self.getArchivePath()))
            return Fail(BoxArchiveHashMismatch("Box archive hash mismatch. Failed download?"))

    def unpackArchive(self):
        return Try.sequence([
            Try.attempt(untar, self.getArchivePath(), self.getPath()),
            Shell.rmFile(self.getArchivePath())
        ])
