
from __future__ import absolute_import

import logging
import threading
import time

from vdsm import utils
from vdsm.common import concurrent
from vdsm.config import config
from vdsm.storage import check
from vdsm.storage import clusterlock
from vdsm.storage import misc
from vdsm.storage.sdc import sdCache

log = logging.getLogger('storage.Monitor')


class Status(object):

    def __init__(self, path_status, domain_status):
        self._path_status = path_status
        self._domain_status = domain_status
        self._time = time.time()

    @property
    def actual(self):
        if not self.valid:
            return True
        return self._path_status.actual and self._domain_status.actual

    @property
    def error(self):
        return self._path_status.error or self._domain_status.error

    @property
    def valid(self):
        return self.error is None

    @property
    def checkTime(self):
        return self._time

    @property
    def readDelay(self):
        return self._path_status.readDelay

    @property
    def diskUtilization(self):
        return self._domain_status.diskUtilization

    @property
    def masterMounted(self):
        return self._domain_status.masterMounted

    @property
    def masterValid(self):
        return self._domain_status.masterValid

    @property
    def hasHostId(self):
        return self._domain_status.hasHostId

    @property
    def vgMdUtilization(self):
        return self._domain_status.vgMdUtilization

    @property
    def vgMdHasEnoughFreeSpace(self):
        return self._domain_status.vgMdHasEnoughFreeSpace

    @property
    def vgMdFreeBelowThreashold(self):
        return self._domain_status.vgMdFreeBelowThreashold

    @property
    def isoPrefix(self):
        return self._domain_status.isoPrefix

    @property
    def version(self):
        return self._domain_status.version


class PathStatus(object):

    def __init__(self, readDelay=0, error=None, actual=True):
        self.readDelay = readDelay
        self.error = error
        self.actual = actual


class DomainStatus(object):

    def __init__(self, error=None, actual=True):
        self.error = error
        self.actual = actual
        self.diskUtilization = (None, None)
        self.masterMounted = False
        self.masterValid = False
        self.hasHostId = False
        self.vgMdUtilization = (0, 0)
        self.vgMdHasEnoughFreeSpace = True
        self.vgMdFreeBelowThreashold = True
        self.isoPrefix = None
        self.version = -1


class DomainMonitor(object):

    def __init__(self, interval):
        self._monitors = {}
        self._interval = interval
        self.onDomainStateChange = misc.Event(
            "storage.DomainMonitor.onDomainStateChange", sync=False)
        self._checker = check.CheckService()
        self._checker.start()

    @property
    def domains(self):
        return self._monitors.keys()

    @property
    def poolDomains(self):
        return [sdUUID for sdUUID, monitor in self._monitors.items()
                if monitor.poolDomain]

    def startMonitoring(self, sdUUID, hostId, poolDomain=True):
        monitor = self._monitors.get(sdUUID)

        if monitor is not None:
            if not poolDomain:
                log.debug("Monitor for %s is already running", sdUUID)
                return

            if monitor.poolDomain:
                log.warning("Monitor for %s is already attached to pool",
                            sdUUID)
                return

            log.info("Attaching monitor for %s to the pool", sdUUID)
            monitor.poolDomain = True
            return

        log.info("Start monitoring %s", sdUUID)
        monitor = MonitorThread(sdUUID, hostId, self._interval,
                                self.onDomainStateChange, self._checker)
        monitor.poolDomain = poolDomain
        monitor.start()
        self._monitors[sdUUID] = monitor

    def stopMonitoring(self, sdUUIDs):
        sdUUIDs = frozenset(sdUUIDs)
        monitors = [monitor for monitor in self._monitors.values()
                    if monitor.sdUUID in sdUUIDs]
        self._stopMonitors(monitors)

    def isMonitoring(self, sdUUID):
        return sdUUID in self._monitors

    def getDomainsStatus(self):
        for sdUUID, monitor in self._monitors.items():
            yield sdUUID, monitor.getStatus()

    def getHostStatus(self, domains):
        status = {}
        for sdUUID, hostId in domains.iteritems():
            try:
                monitor = self._monitors[sdUUID]
            except KeyError:
                status[sdUUID] = clusterlock.HOST_STATUS_UNAVAILABLE
            else:
                status[sdUUID] = monitor.getHostStatus(hostId)
        return status

    def getHostId(self, sdUUID):
        return self._monitors[sdUUID].hostId

    def shutdown(self):
        log.info("Shutting down domain monitors")
        self._stopMonitors(self._monitors.values(), shutdown=True)
        self._checker.stop()

    def _stopMonitors(self, monitors, shutdown=False):

        for monitor in monitors:
            log.info("Stop monitoring %s (shutdown=%s)",
                     monitor.sdUUID, shutdown)
            monitor.stop(shutdown=shutdown)

        for monitor in monitors:
            log.debug("Waiting for monitor %s", monitor.sdUUID)
            monitor.join()
            try:
                del self._monitors[monitor.sdUUID]
            except KeyError:
                log.warning("Montior for %s removed while stopping",
                            monitor.sdUUID)


class MonitorThread(object):

    def __init__(self, sdUUID, hostId, interval, changeEvent, checker):
        self.thread = concurrent.thread(self._run, log=log,
                                        name="monitor/" + sdUUID[:7])
        self.stopEvent = threading.Event()
        self.domain = None
        self.sdUUID = sdUUID
        self.hostId = hostId
        self.interval = interval
        self.changeEvent = changeEvent
        self.checker = checker
        self.lock = threading.Lock()
        self.monitoringPath = None
        self.status = Status(PathStatus(actual=False),
                             DomainStatus(actual=False))
        self.isIsoDomain = None
        self.isoPrefix = None
        self.lastRefresh = time.time()
        self.refreshTime = \
            config.getfloat("irs", "repo_stats_cache_refresh_timeout")
        self.wasShutdown = False
        self.cycleCallback = _NULL_CALLBACK

    def start(self):
        self.thread.start()

    def stop(self, shutdown=False):
        self.wasShutdown = shutdown
        self.stopEvent.set()

    def join(self):
        self.thread.join()

    def getStatus(self):
        return self.status

    def getHostStatus(self, hostId):
        if not self.domain:
            return clusterlock.HOST_STATUS_UNAVAILABLE
        return self.domain.getHostStatus(hostId)

    def __canceled__(self):
        """ Accessed by methods decorated with @util.cancelpoint """
        return self.stopEvent.is_set()

    def _run(self):
        log.debug("Domain monitor for %s started", self.sdUUID)
        try:
            self._setupLoop()
            self._monitorLoop()
        except utils.Canceled:
            log.debug("Domain monitor for %s canceled", self.sdUUID)
        finally:
            log.debug("Domain monitor for %s stopped (shutdown=%s)",
                      self.sdUUID, self.wasShutdown)
            self._stopCheckingPath()
            if self._shouldReleaseHostId():
                self._releaseHostId()


    def _setupLoop(self):
        while True:
            try:
                self._setupMonitor()
                return
            except Exception as e:
                log.exception("Setting up monitor for %s failed", self.sdUUID)
                domain_status = DomainStatus(error=e)
                status = Status(self.status._path_status, domain_status)
                self._updateStatus(status)
                self.cycleCallback()
                if self.stopEvent.wait(self.interval):
                    raise utils.Canceled

    def _setupMonitor(self):
        if self._shouldRefreshDomain():
            self._refreshDomain()

        if self.domain is None:
            self._produceDomain()

        if self.monitoringPath is None:
            self.monitoringPath = self.domain.getMonitoringPath()
            self.checker.start_checking(self.monitoringPath, self._pathChecked,
                                        interval=self.interval)

        if self.isIsoDomain is None:
            self._setIsoDomainInfo()

    @utils.cancelpoint
    def _produceDomain(self):
        log.debug("Producing domain %s", self.sdUUID)
        self.domain = sdCache.produce(self.sdUUID)

    @utils.cancelpoint
    def _setIsoDomainInfo(self):
        isIsoDomain = self.domain.isISO()
        if isIsoDomain:
            log.debug("Domain %s is an ISO domain", self.sdUUID)
            self.isoPrefix = self.domain.getIsoDomainImagesDir()
        self.isIsoDomain = isIsoDomain


    def _monitorLoop(self):
        while True:
            try:
                self._monitorDomain()
            except Exception:
                log.exception("Domain monitor for %s failed", self.sdUUID)
            finally:
                self.cycleCallback()
            if self.stopEvent.wait(self.interval):
                raise utils.Canceled

    def _monitorDomain(self):
        if self._shouldRefreshDomain():
            self._refreshDomain()

        self._checkDomainStatus()

    @utils.cancelpoint
    def _checkDomainStatus(self):
        domain_status = DomainStatus()
        try:
            self.domain.selftest()

            stats = self.domain.getStats()
            domain_status.diskUtilization = (stats["disktotal"],
                                             stats["diskfree"])

            domain_status.vgMdUtilization = (stats["mdasize"],
                                             stats["mdafree"])
            domain_status.vgMdHasEnoughFreeSpace = stats["mdavalid"]
            domain_status.vgMdFreeBelowThreashold = stats["mdathreshold"]

            masterStats = self.domain.validateMaster()
            domain_status.masterValid = masterStats['valid']
            domain_status.masterMounted = masterStats['mount']

            domain_status.hasHostId = self._domainHasHostId()
            domain_status.version = self.domain.getVersion()
            domain_status.isoPrefix = self.isoPrefix
        except Exception as e:
            log.exception("Error checking domain %s", self.sdUUID)
            domain_status.error = e

        with self.lock:
            status = Status(self.status._path_status, domain_status)
            self._updateStatus(status)

        if self._shouldAcquireHostId():
            self._acquireHostId()

    def _domainHasHostId(self):
        hasHostId = self.domain.hasHostId(self.hostId)
        if not hasHostId and self.status.hasHostId:
            log.warning("Host id for domain %s was released (id: %s)",
                        self.sdUUID, self.hostId)
        if hasHostId and not self.status.hasHostId:
            log.info("Host id for domain %s successfully acquired (id: %s)",
                     self.sdUUID, self.hostId)
        return hasHostId


    def _updateStatus(self, status):
        if self._statusDidChange(status):
            self._notifyStatusChanges(status)
        self.status = status

    def _statusDidChange(self, status):
        if not status.actual:
            return False
        if not self.status.actual:
            return True
        return self.status.valid != status.valid

    def _notifyStatusChanges(self, status):
        log.info("Domain %s became %s", self.sdUUID,
                 "VALID" if status.valid else "INVALID")
        try:
            self.changeEvent.emit(self.sdUUID, status.valid)
        except:
            log.exception("Error notifying state change for domain %s",
                          self.sdUUID)


    def _shouldRefreshDomain(self):
        return time.time() - self.lastRefresh > self.refreshTime

    @utils.cancelpoint
    def _refreshDomain(self):
        log.debug("Refreshing domain %s", self.sdUUID)
        sdCache.manuallyRemoveDomain(self.sdUUID)
        self.lastRefresh = time.time()


    def _pathChecked(self, result):
        try:
            delay = result.delay()
        except Exception as e:
            log.exception("Error checking path %s", self.monitoringPath)
            path_status = PathStatus(error=e)
        else:
            path_status = PathStatus(readDelay=delay)

        with self.lock:
            status = Status(path_status, self.status._domain_status)
            self._updateStatus(status)

    def _stopCheckingPath(self):
        if self.monitoringPath is None:
            return
        try:
            self.checker.stop_checking(self.monitoringPath, timeout=1.0)
        except Exception:
            log.exception("Error stopping checker for %s", self.monitoringPath)


    def _shouldAcquireHostId(self):
        if self.isIsoDomain:
            return False

        if not self.status.actual:
            return False

        if not self.status.valid:
            return False

        return self.status.hasHostId is False

    def _shouldReleaseHostId(self):
        if not self.domain:
            return False

        if self.isIsoDomain:
            return False

        if self.wasShutdown:
            return False

        if not self.status.actual:
            return False

        return True

    @utils.cancelpoint
    def _acquireHostId(self):
        try:
            self.domain.acquireHostId(self.hostId, wait=False)
        except:
            log.exception("Error acquiring host id %s for domain %s",
                          self.hostId, self.sdUUID)

    def _releaseHostId(self):
        try:
            self.domain.releaseHostId(self.hostId, unused=True)
        except:
            log.exception("Error releasing host id %s for domain %s",
                          self.hostId, self.sdUUID)


def _NULL_CALLBACK():
    pass
