"""Base for all node resource services.
"""

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals

import abc
import collections
import errno
import glob
import io
import logging
import os
import socket
import struct
import tempfile
import time

import six

from treadmill import dirwatch
from treadmill import exc
from treadmill import fs
from treadmill import logcontext as lc
from treadmill import plugin_manager
from treadmill import utils
from treadmill import watchdog
from treadmill import yamlwrapper as yaml


_LOGGER = logging.getLogger(__name__)

#: Name of the directory holding the resources requests
RSRC_DIR = 'resources'
#: Name of request payload file
REQ_FILE = 'request.yml'
#: Name of reply payload file
REP_FILE = 'reply.yml'
#: Default Resource Service timeout
DEFAULT_TIMEOUT = 15 * 60


def wait_for_file(filename, timeout=None):
    """Wait at least ``timeout`` seconds for a file to appear or be modified.

    :param ``int`` timeout:
        Minimum amount of seconds to wait for the file.
    :returns ``bool``:
        ``True`` if there was an event, ``False`` otherwise (timeout).
    """
    if timeout is None:
        timeout = DEFAULT_TIMEOUT

    elif timeout == 0:
        return os.path.exists(filename)

    filedir = os.path.dirname(filename)
    # TODO: Fine tune the watcher mask for efficiency.
    watcher = dirwatch.DirWatcher(filedir)

    now = time.time()
    end_time = now + timeout
    while not os.path.exists(filename):
        if watcher.wait_for_events(timeout=max(0, end_time - now)):
            watcher.process_events()

        now = time.time()
        if now > end_time:
            return False

    return True


class ResourceServiceError(exc.TreadmillError):
    """Base Resource Service error.
    """
    __slots__ = ()


class ResourceServiceRequestError(ResourceServiceError):
    """Resource Service Request error.
    """
    __slots__ = (
        'request',
    )

    def __init__(self, message, request):
        super(ResourceServiceRequestError, self).__init__(message)
        self.request = request


class ResourceServiceTimeoutError(ResourceServiceError, socket.timeout):
    """Resource Service timeout.
    """
    __slots__ = ()


class ResourceServiceClient(object):
    """Client class for all Treadmill services.

    /apps/<container>/rsrc/req-<svc_name>/
        request.yml
        reply.yml
        svc_req_id
    """

    _REQ_UID_FILE = 'svc_req_id'

    __slots__ = (
        '_serviceinst',
        '_clientdir',
    )

    def __init__(self, serviceinst, clientdir):
        self._serviceinst = serviceinst
        fs.mkdir_safe(clientdir)
        self._clientdir = os.path.realpath(clientdir)

    def put(self, rsrc_id, rsrc_data):
        """Request creation/update of a resource.

        :param `str` rsrc_id:
            Unique identifier for the requested resource.
        :param `str` rsrc_data:
            (New) Parameters for the requested resource.
        """
        req_dir = self._req_dirname(rsrc_id)
        fs.mkdir_safe(req_dir)

        with io.open(os.path.join(req_dir, REQ_FILE), 'w') as f:
            if os.name == 'posix':
                os.fchmod(f.fileno(), 0o644)
            yaml.dump(rsrc_data,
                      explicit_start=True, explicit_end=True,
                      default_flow_style=False,
                      stream=f)

        req_uuid_file = os.path.join(req_dir, self._REQ_UID_FILE)
        try:
            with io.open(req_uuid_file) as f:
                svc_req_uuid = f.read().strip()
        except IOError as err:
            if err.errno == errno.ENOENT:
                svc_req_uuid = None
            else:
                raise

        with lc.LogContext(_LOGGER, rsrc_id):
            if svc_req_uuid is None:
                try:
                    # New request
                    svc_req_uuid = self._serviceinst.clt_new_request(rsrc_id,
                                                                     req_dir)
                    # Write down the UUID
                    with io.open(req_uuid_file, 'w') as f:
                        f.write(svc_req_uuid)
                        os.fchmod(f.fileno(), 0o644)

                except OSError:
                    # Error registration failed, delete the request.
                    _LOGGER.exception('Unable to submit request')
                    fs.rmtree_safe(req_dir)

            else:
                self._serviceinst.clt_update_request(svc_req_uuid)

    def delete(self, rsrc_id):
        """Delete an existing resource.

        :param `str` rsrc_id:
            Unique identifier for the requested resource.
        """
        with lc.LogContext(_LOGGER, rsrc_id,
                           adapter_cls=lc.ContainerAdapter) as log:
            req_dir = self._req_dirname(rsrc_id)
            try:
                with io.open(os.path.join(req_dir, self._REQ_UID_FILE)) as f:
                    svc_req_uuid = f.read().strip()
            except IOError as err:
                if err.errno == errno.ENOENT:
                    log.warning('Resource %r does not exist', rsrc_id)
                    return
                raise
            self._serviceinst.clt_del_request(svc_req_uuid)
            os.rename(
                req_dir,
                self._bck_dirname(svc_req_uuid)
            )

    def get(self, rsrc_id):
        """Get the result of a resource request.

        :param `str` rsrc_id:
            Unique identifier for the requested resource.
        :raises ``ResourceServiceRequestError``:
            If the request resulted in error.
        """
        try:
            res = self.wait(rsrc_id, timeout=0)
        except ResourceServiceTimeoutError:
            res = None
        return res

    def wait(self, rsrc_id, timeout=None):
        """Wait for a requested resource to be ready.

        :param `str` rsrc_id:
            Unique identifier for the requested resource.
        :raises ``ResourceServiceRequestError``:
            If the request resulted in error.
        :raises ``ResourceServiceTimeoutError``:
            If the request was not available before timeout.
        """
        req_dir = self._req_dirname(rsrc_id)
        rep_file = os.path.join(req_dir, REP_FILE)

        if not wait_for_file(rep_file, timeout):
            raise ResourceServiceTimeoutError(
                'Resource %r not available in time' % rsrc_id
            )

        try:
            with io.open(rep_file) as f:
                reply = yaml.load(stream=f)

        except (IOError, OSError) as err:
            if err.errno == errno.ENOENT: