__author__ = 'aherman'

import json
import datetime
import re
import types
import packagingException


class CanBeEnabled(object):
    def __init__(self):
        self.enabled = None


class JMXMonitoringConfig(CanBeEnabled):
    def __init__(self, port):
        super(JMXMonitoringConfig, self).__init__()
        self.port = port

    def merge(self, child):
        """
        :type child: JMXMonitoringConfig
        """
        if child is None:
            return self

        result = JMXMonitoringConfig()
        result.port = _getNotNone(child.port, self.port)
        return result

class LoggingConfig(object):
    def __init__(self, configFile=None, redirectOutput=None):
        self.configFile = configFile
        self.redirectOutput = redirectOutput

    def merge(self, child):
        """
        :type child: LoggingConfig
        """
        if child is None:
            return self

        return LoggingConfig(
            configFile=_getNotNone(child.configFile, self.configFile),
            redirectOutput=_getNotNone(child.redirectOutput, self.redirectOutput)
        )

class MonitoringConfig(object):
    def __init__(self):
        self.jmxMonitoringConfig = None

    def merge(self, child):
        """
        :type child: MonitoringConfig
        """
        if child is None:
            return self

        result = MonitoringConfig()
        if self.jmxMonitoringConfig is None:
            result.jmxMonitoringConfig = child.jmxMonitoringConfig
        else:
            result.jmxMonitoringConfig = self.jmxMonitoringConfig.merge(child.jmxMonitoringConfig)

        return result


class ProfilingConfig(CanBeEnabled):
    def __init__(self, agentPath):
        super(ProfilingConfig, self).__init__()
        self.agentPath = agentPath
        self.options = {}

    def merge(self, child):
        """
        :type child: ProfilingConfig
        """
        if child is None:
            return self

        result = ProfilingConfig()
        result.agentPath = _getNotNone(child.agentPath, self.agentPath)
        result.options = _getNotNone(child.options, self.options)
        return result

    def getOptionsAsString(self):
        result = []
        for k, v in self.options.iteritems():
            if v is None:
                result.append(k)
            else:
                result.append("{0}={1}".format(k, v))
        return ",".join(result)


class DebugConfig(CanBeEnabled):
    def __init__(self, port):
        super(DebugConfig, self).__init__()
        self.port = port

    def merge(self, child):
        """
        :type child: DebugConfig
        """
        if child is None:
            return self

        result = DebugConfig()
        result.port = _getNotNone(child.port, self.port)

        return result

class MemcachedConfig(object):
    def __init__(self, flush_on_restart):
        self.flush_on_restart = flush_on_restart

    def merge(self, child):
        """
        :type child: MemcachedConfig
        """
        if child is None:
            return self

        result = MemcachedConfig()
        result.flush_on_restart = _getNotNone(child.flush_on_restart, self.flush_on_restart)

        return result


class DeploymentConfig(object):
    def __init__(self, host, path, rootPath):
        """
        :type host: str
        :type path: str
        :type rootPath: str
        """
        self.host = host
        self._path = path
        self._rootPath = rootPath

    def getPath(self, applicationConfig):
        """
        :type applicationConfig: ApplicationConfig
        """
        if self._path is not None:
            return self._path

        if self._rootPath is not None:
            return "{0}/{1}-{2}".format(self._rootPath, applicationConfig.project, applicationConfig.module)

        return None


class JavaConfig(object):
    def __init__(self, parentApplicationConfig):
        """
        :type parentApplicationConfig: ApplicationConfig
        """
        self.mainClass = None
        self.xms = None
        self.xmx = None
        self.version = None
        self.options = None
        self.useG1GC = False

        self.parentApplicationConfig = parentApplicationConfig

    def merge(self, child, newApplicationConfig):
        """
        :type child: JavaConfig
        :type newApplicationConfig: ApplicationConfig
        """
        if child is None:
            result = JavaConfig(newApplicationConfig)
            result.mainClass = self.mainClass
            result.xms = self.xms
            result.xmx = self.xmx
            result.version = self.version
            result.options = self.options
            result.useG1GC = self.useG1GC
            return result

        result = JavaConfig(newApplicationConfig)
        result.mainClass = _getNotNone(child.mainClass, self.mainClass)
        result.xms = _getNotNone(child.xms, self.xms)
        result.xmx = _getNotNone(child.xmx, self.xmx)
        result.version = _getNotNone(child.version, self.version)
        result.options = _getNotNone(child.options, self.options)
        result.useG1GC = _getNotNone(child.useG1GC, self.useG1GC)
        return result


class FoldersConfig(object):
    def __init__(self):
        self.other = None

    def merge(self, child):
        """

        :type child: FoldersConfig
        """
        if child is None:
            return self

        result = FoldersConfig()
        result.other = _getNotNone(child.other, self.other)
        return result


class HttpConfig(object):
    def __init__(self, port, monitoringPort, pingPort):
        self.port = port
        self.monitoringPort = monitoringPort
        self.pingPort = pingPort

    def merge(self, child):
        if child is None:
            return self

        return HttpConfig(child.port, child.monitoringPort, child.pingPort)


class InitDConfig(object):
    def __init__(self):
        self.ubic = False
        self.oldstyle = False
        self.upstart = False
        self.options = None

    def merge(self, child):
        if child is None:
            return self

        result = InitDConfig()
        result.oldstyle = _getNotNone(child.oldstyle, self.oldstyle)
        result.options = _getNotNone(child.options, self.options)

        result.ubic = _getNotNone(child.ubic, self.ubic)
        result.upstart = _getNotNone(child.ubic, self.upstart)

        return result


class DockerConfig(object):
    def __init__(self, image=None):
        self.image = image

    def merge(self, child):
        """
        :type child: DockerConfig
        """
        if (child is None):
            return self

        return DockerConfig(
            image=_getNotNone(child.image, self.image)
        )

class OldsyleInitdOptions(object):
    def __init__(self):
        self.scriptName = None


class InitOptions(object):
    def __init__(self):
        self.scriptName = None


class ApplicationConfig(object):
    def __init__(self):
        self.project = None
        self.module = None
        self.version = None
        self.repositoryUrl = None
        self.repositoryRevision = None
        self.buildDate = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
        self.packageName = None
        self.pingDisableSleep = None

        self.assemblyHelper = None
        self.folderHelper = None
        self.deployMode = None

        self.loggingConfig = None
        self.monitoringConfig = None
        self.profilingConfig = None
        self.debugConfig = None
        self.memcachedConfig = None
        self.deploymentConfig = None
        self.javaConfig = None
        self.foldersConfig = None
        self.logFiles = None
        self.httpConfig = None
        self.initdConfig = None
        self.dockerConfig = None

    def merge(self, child):
        """
        :type child: ApplicationConfig
        """
        if child is None:
            return self

        result = ApplicationConfig()
        result.project = _getNotNone(child.project, self.project)
        result.module = _getNotNone(child.module, self.module)
        result.version = _getNotNone(child.version, self.version)
        result.repositoryUrl = _getNotNone(child.repositoryUrl, self.repositoryUrl)
        result.repositoryRevision = _getNotNone(child.repositoryRevision, self.repositoryRevision)
        result.packageName = _getNotNone(child.packageName, self.packageName)
        result.logFiles = _getNotNone(child.logFiles, self.logFiles)
        result.pingDisableSleep = _getNotNone(child.pingDisableSleep, self.pingDisableSleep)

        result.assemblyHelper = _getNotNone(child.assemblyHelper, self.assemblyHelper)
        result.folderHelper = _getNotNone(child.folderHelper, self.folderHelper)
        result.deployMode = _getNotNone(child.deployMode, self.deployMode)

        if self.loggingConfig is not None:
            result.loggingConfig = self.loggingConfig.merge(child.loggingConfig)
        else:
            result.loggingConfig = child.loggingConfig

        if self.monitoringConfig is not None or child.monitoringConfig is not None:
            if self.monitoringConfig is not None:
                result.monitoringConfig = self.monitoringConfig.merge(child.monitoringConfig)
            else:
                result.monitoringConfig = child.monitoringConfig

        if self.profilingConfig is not None:
            result.profilingConfig = self.profilingConfig.merge(child.profilingConfig)
        else:
            result.profilingConfig = child.profilingConfig

        if self.debugConfig is not None:
            result.debugConfig = self.debugConfig.merge(child.debugConfig)
        else:
            result.debugConfig = child.debugConfig

        if self.memcachedConfig is not None:
            result.memcachedConfig = self.memcachedConfig.merge(child.memcachedConfig)
        else:
            result.memcachedConfig = child.memcachedConfig

        if child.deploymentConfig is not None:
            result.deploymentConfig = child.deploymentConfig

        if self.javaConfig is not None:
            result.javaConfig = self.javaConfig.merge(child.javaConfig, result)
        else:
            result.javaConfig = child.javaConfig

        if self.foldersConfig is not None:
            result.foldersConfig = self.foldersConfig.merge(child.foldersConfig)
        else:
            result.foldersConfig = child.foldersConfig

        if self.httpConfig is not None:
            result.httpConfig = self.httpConfig.merge(child.httpConfig)
        else:
            result.httpConfig = child.httpConfig

        if self.initdConfig is not None:
            result.initdConfig = self.initdConfig.merge(child.initdConfig)
        else:
            result.initdConfig = child.initdConfig

        if self.dockerConfig is not None:
            result.dockerConfig = self.dockerConfig.merge(child.dockerConfig)
        else:
            result.dockerConfig = child.dockerConfig

        return result


class ApplicationConfigParserException(Exception):
    def __init__(self, value):
        self.value = value

    def __str__(self):
        return repr(self.value)


def parseApplicationConfigJson(jsonFile, applicationConfig):
    jsonData = json.load(jsonFile)
    applicationConfig.project = _getValue(jsonData, "project", configName="/", required=False)
    applicationConfig.module = _getValue(jsonData, "module", configName="/", required=False)
    applicationConfig.logFiles = _getValue(jsonData, "log-files", configName="/", required=False)
    applicationConfig.pingDisableSleep = _getValue(jsonData, "ping_disable_sleep", configName="/", required=False)

    if "logging" in jsonData:
        applicationConfig.loggingConfig = _parseLogging(jsonData["logging"])

    if "monitoring" in jsonData:
        applicationConfig.monitoringConfig = _parseMonitoring(jsonData["monitoring"])

    if "profiling" in jsonData:
        applicationConfig.profilingConfig = _parseProfiling(jsonData["profiling"])

    if "debug" in jsonData:
        applicationConfig.debugConfig = _parseDebug(jsonData["debug"])

    if "memcached" in jsonData:
        applicationConfig.memcachedConfig = _parseMemcached(jsonData["memcached"])

    if "deployment" in jsonData:
        applicationConfig.deploymentConfig = _parseDeployment(jsonData["deployment"])

    if "java" in jsonData:
        applicationConfig.javaConfig = _parseJava(jsonData["java"], applicationConfig)

    if "folders" in jsonData:
        applicationConfig.foldersConfig = _parseFoldersConfig(jsonData["folders"])

    if "http" in jsonData:
        applicationConfig.httpConfig = _parseHttpConfig(jsonData["http"])

    if "initd" in jsonData:
        applicationConfig.initdConfig = _parseInitDConfig(jsonData["initd"])

    if "docker" in jsonData:
        applicationConfig.dockerConfig = _parseDockerConfig(jsonData["docker"])

def _parseLogging(jsonData):
    return LoggingConfig(
        configFile=_getValue(jsonData, "configFile", None, "/logging"),
        redirectOutput=_getValue(jsonData, "redirectOutput", None, "/logging")
    )

def _parseMonitoring(jsonData):
    if "jmx" not in jsonData:
        return None

    jmxJsonData = jsonData["jmx"]
    port = _getValue(jmxJsonData, "port", None, "/monitoring/jmx")
    jmxMonitoringConfig = JMXMonitoringConfig(port)
    jmxMonitoringConfig.enabled = _isEnabledByDefault(jmxJsonData)

    monitoringConfig = MonitoringConfig()
    monitoringConfig.jmxMonitoringConfig = jmxMonitoringConfig
    return monitoringConfig


def _parseProfiling(jsonData):
    agentPath = _getValue(jsonData, "agent-path", None, "/profiling")
    options = jsonData["options"] if "options" in jsonData else {}
    profilingConfig = ProfilingConfig(agentPath)
    profilingConfig.enabled = _isEnabledByDefault(jsonData)
    profilingConfig.options = options
    return profilingConfig


def _parseDebug(jsonData):
    port = _getValue(jsonData, "port", None, "/debug")
    debugConfig = DebugConfig(port)
    debugConfig.enabled = _isEnabledByDefault(jsonData)
    return debugConfig

def _parseMemcached(jsonData):
    flush_on_restart = _getValue(jsonData, "flush-on-restart", None, "/memcached")
    memcachedConfig = MemcachedConfig(flush_on_restart)
    memcachedConfig.enabled = _isEnabledByDefault(jsonData)
    return memcachedConfig



def _parseDeployment(jsonData):
    host = _getValue(jsonData, "host", configName="/deployment")
    path = _getValue(jsonData, "path", configName="/deployment", required=False)
    rootPath = _getValue(jsonData, "root-path", configName="/deployment", required=False)
    deploymentConfig = DeploymentConfig(host, path, rootPath)
    return deploymentConfig


def _parseJava(jsonData, applicationConfig):
    javaConfig = JavaConfig(applicationConfig)
    javaConfig.xms = _getValue(jsonData, "xms", configName="/java")
    javaConfig.xmx = _getValue(jsonData, "xmx", configName="/java")
    if javaConfig.xms is not None or javaConfig.xmx is not None:
        if javaConfig.xms is None or javaConfig.xmx is None:
            raise packagingException.PackagingException("/java/xms and /java/xmx must be provided simultaneously")
    javaConfig.mainClass = _getValue(jsonData, "main-class", configName="/java", required=False)
    javaConfig.version = _getValue(jsonData, "version", configName="/java", required=False)
    javaConfig.options = _getValue(jsonData, "options", {}, configName="/java")
    javaConfig.useG1GC = _getValue(jsonData, "use-g1gc", configName="/java", required=False)
    return javaConfig


def _parseFoldersConfig(jsonData):
    foldersConfig = FoldersConfig()
    foldersConfig.other = _getValue(jsonData, "other", configName="/folders", required=False)
    return foldersConfig


def _parseHttpConfig(jsonData):
    port = _getValue(jsonData, "port", configName="/http")
    monitoringPort = _getValue(jsonData, "monitoringPort", configName="/http", required=False)
    pingPort = _getValue(jsonData, "pingPort", default=monitoringPort, configName="/http", required=False)
    return HttpConfig(port, monitoringPort, pingPort)


def _parseInitDConfig(jsonData):
    oldstyleConf = _getValue(jsonData, "oldstyle", configName="/initd", required=False)
    ubicConf = _getValue(jsonData, "ubic", configName="/initd", required=False)
    upstartConf = _getValue(jsonData, "upstart", configName="/initd", required=False)

    result = InitDConfig()
    result.options = InitOptions()
    if ubicConf is not None:
        result.ubic = True
        if "script-name" in ubicConf:
            result.options.scriptName = ubicConf["script-name"]
    elif upstartConf is not None:
        result.upstart = True
    elif oldstyleConf is not None:
        result.oldstyle = True
        if "script-name" in oldstyleConf:
            result.options.scriptName = oldstyleConf["script-name"]
    else:
        # Default is oldstyle
        result.oldstyle = True
        result.options = InitOptions()


    return result


def _parseDockerConfig(jsonData):
    return DockerConfig(
        image=_getValue(jsonData, "image", None, configName="/docker", )
    )


def _isEnabledByDefault(jsonData):
    return "enabled-by-default" in jsonData and jsonData["enabled-by-default"]


def _getValue(jsonData, name, default=None, configName="", required=True):
    if name not in jsonData:
        if default is not None:
            return default
        elif required:
            message = "Mandatory parameter \"" + name + "\" not found" + (" in " + configName if configName else "")
            raise ApplicationConfigParserException(message)
        else:
            return None

    return jsonData[name]


def mergeConfigs(parent, child):
    """

    :type parent: ApplicationConfig
    :type child: ApplicationConfig
    """
    return parent.merge(child)


def _getNotNone(first, second):
    if first is not None:
        return first
    else:
        return second


def updateProjectModuleName(moduleName, applicationConfig):
    if applicationConfig.project is not None and applicationConfig.module is not None:
        return
    hyphen = moduleName.index('-')
    applicationConfig.project = moduleName[0: hyphen]
    applicationConfig.module = moduleName[hyphen + 1:]


def updatePackageAndVersion(applicationConfig):
    changelogFile = applicationConfig.assemblyHelper.changelogPath
    versionLine = ""
    with open(changelogFile) as f:
        versionLine = f.readline()
    if not len(versionLine):
        raise ApplicationConfigParserException("First changelog line is empty")

    versionLineParts = versionLine.split(' ')
    if not len(versionLineParts):
        raise ApplicationConfigParserException("Unknown changelog format: {0}".format(versionLine))

    applicationConfig.packageName = versionLineParts[0]

    version = re.search(r'\(([^)]+)\)', versionLineParts[1])
    if not version:
        raise ApplicationConfigParserException("Unable to find version: {0}".format(versionLine))
    applicationConfig.version = version.group(1)


def resolveProperties(applicationConfig):
    """
    :type applicationConfig: ApplicationConfig
    """
    if applicationConfig.javaConfig is not None:
        mainClass = applicationConfig.javaConfig.mainClass
        applicationConfig.javaConfig.mainClass = _resolveProperties(mainClass, applicationConfig)

    if applicationConfig.deploymentConfig is not None:
        path = applicationConfig.deploymentConfig._path
        applicationConfig.deploymentConfig._path = _resolveProperties(path, applicationConfig)

    if applicationConfig.foldersConfig is not None:
        other = applicationConfig.foldersConfig.other
        if other is not None:
            fixedOther = {}
            for path, value in other.iteritems():
                path = _resolveProperties(path, applicationConfig)
                fixedOther[path] = value
            applicationConfig.foldersConfig.other = fixedOther

    if applicationConfig.logFiles is not None:
        resolvedFiles = []
        for f in applicationConfig.logFiles:
            resolvedFiles.append(_resolveProperties(f, applicationConfig))
        applicationConfig.logFiles = resolvedFiles

    if applicationConfig.javaConfig is not None:
        options = applicationConfig.javaConfig.options
        if options is not None:
            fixedOptions = {}
            for name, value in options.iteritems():
                fixedOptions[name] = _resolveProperties(value, applicationConfig)
            applicationConfig.javaConfig.options = fixedOptions

    if applicationConfig.loggingConfig is not None:
        if applicationConfig.loggingConfig.configFile is not None:
            configFile = applicationConfig.loggingConfig.configFile
            applicationConfig.loggingConfig.configFile = _resolveProperties(configFile, applicationConfig)

    if applicationConfig.initdConfig:
        if applicationConfig.initdConfig.options:
            if applicationConfig.initdConfig.options.scriptName:
                applicationConfig.initdConfig.options.scriptName = _resolveProperties(
                    applicationConfig.initdConfig.options.scriptName, applicationConfig)


def setDefaults(applicationConfig):
    """
    :type applicationConfig: ApplicationConfig
    """
    if applicationConfig.loggingConfig is None:
        applicationConfig.loggingConfig = LoggingConfig()

    if applicationConfig.loggingConfig.configFile is None:
        applicationConfig.loggingConfig.configFile = "{0}/logback-config.xml".format(applicationConfig.folderHelper.etcFolder)

    if applicationConfig.loggingConfig.redirectOutput is None:
        applicationConfig.loggingConfig.redirectOutput = True

    if applicationConfig.javaConfig is None:
        applicationConfig.javaConfig = JavaConfig()

    if applicationConfig.javaConfig.xms is None:
        applicationConfig.javaConfig.xms = "1G"
        applicationConfig.javaConfig.xmx = "1G"

    if applicationConfig.javaConfig.mainClass is None:
        applicationConfig.javaConfig.mainClass = "ru.yandex.{0}.{1}.Main".format(applicationConfig.project, applicationConfig.module)

    if applicationConfig.javaConfig.options is None:
        applicationConfig.javaConfig.options = {}

    if applicationConfig.logFiles is None:
        applicationConfig.logFiles = []
        applicationConfig.logFiles.append("{0}/{1}".format(applicationConfig.folderHelper.logFolder, applicationConfig.folderHelper.logFile))

    if applicationConfig.initdConfig is None:
        applicationConfig.initdConfig = InitDConfig()
        applicationConfig.initdConfig.oldstyle = True
        applicationConfig.initdConfig.options = InitOptions()

    if applicationConfig.dockerConfig is None:
        applicationConfig.dockerConfig = DockerConfig()

    if applicationConfig.dockerConfig.image is None:
        applicationConfig.dockerConfig.image = "ubuntu:12.04"


def _resolveProperties(value, applicationConfig):
    """
    :type value: str
    """
    if value is None:
        return value

    if not isinstance(value, basestring):
        return value

    value = value.replace("${application.project}", applicationConfig.project)
    value = value.replace("${application.module}", applicationConfig.module)
    value = value.replace("${folders.data}", applicationConfig.folderHelper.dataFolder)
    value = value.replace("${folders.etc}", applicationConfig.folderHelper.etcFolder)
    value = value.replace("${folders.log}", applicationConfig.folderHelper.logFolder)
    value = value.replace("${folders.lib}", applicationConfig.folderHelper.libFolder)
    value = value.replace("${http.monitoringPort}", str(applicationConfig.httpConfig.monitoringPort))
    value = value.replace("${http.pingPort}", str(applicationConfig.httpConfig.pingPort))
    return value
