# Copyright (c) 2016 IBM Corp.
# Copyright 2017 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

import logging
import os
import os.path
import re
import select
import threading
import time

from zuul.lib import streamer_utils


class Log(object):

    def __init__(self, path):
        self.path = path
        # The logs are written as binary encoded utf-8, which is what we
        # send over the wire.
        self.file = open(path, 'rb')
        self.stat = os.stat(path)
        self.size = self.stat.st_size


class RequestHandler(streamer_utils.BaseFingerRequestHandler):
    '''
    Class to handle a single log streaming request.

    The log streaming code was blatantly stolen from zuul_console.py. Only
    the (class/method/attribute) names were changed to protect the innocent.
    '''

    log = logging.getLogger("zuul.log_streamer")

    def handle(self):
        try:
            build_uuid = self.getCommand()
        except BrokenPipeError:
            self.log.info("Broken pipe")
            # The stream was disconnected
            return
        except Exception:
            self.log.exception("Failure during getCommand:")
            msg = 'Internal streaming error'
            self.request.sendall(msg.encode("utf-8"))
            return

        # validate build ID
        if not re.match("[0-9A-Fa-f]+$", build_uuid):
            msg = 'Build ID %s is not valid' % build_uuid
            self.request.sendall(msg.encode("utf-8"))
            return

        job_dir = os.path.join(self.server.jobdir_root, build_uuid)
        if not os.path.exists(job_dir):
            msg = 'Build ID %s not found' % build_uuid
            self.request.sendall(msg.encode("utf-8"))
            return

        # check if log file exists
        log_file = os.path.join(job_dir, 'work', 'logs', 'job-output.txt')
        if not os.path.exists(log_file):
            msg = 'Log not found for build ID %s' % build_uuid
            self.request.sendall(msg.encode("utf-8"))
            return

        try:
            self.stream_log(log_file)
        except (ConnectionResetError, BrokenPipeError) as e:
            self.log.error("Streaming failure for build UUID %s: %s",
                           build_uuid, str(e))
        except Exception:
            self.log.exception("Streaming failure for build UUID %s:",
                               build_uuid)
            msg = 'Internal streaming error'
            self.request.sendall(msg.encode("utf-8"))

    def stream_log(self, log_file):
        log = None
        while True:
            if log is not None:
                try:
                    log.file.close()
                except Exception:
                    pass
            while True:
                log = self.chunk_log(log_file)
                if log:
                    break
                time.sleep(0.5)
            while True:
                if self.follow_log(log):
                    break
                else:
                    if log is not None:
                        try:
                            log.file.close()
                        except Exception:
                            pass
                    return

    def chunk_log(self, log_file):
        try:
            log = Log(log_file)
        except Exception:
            return
        while True:
            chunk = log.file.read(4096)
            if not chunk:
                break
            self.request.send(chunk)
        return log

    def follow_log(self, log):
        while True:
            # As long as we have unread data, keep reading/sending
            while True:
                chunk = log.file.read(4096)
                if chunk:
                    self.request.send(chunk)
                else:
                    break

            # See if the file has been removed, meaning we should stop
            # streaming it.
            if not os.path.exists(log.path):
                return False

            # At this point, we are waiting for more data to be written
            time.sleep(0.5)

            # Check to see if the remote end has sent any data, if so,
            # discard
            r, w, e = select.select([self.request], [], [self.request], 0)
            if self.request in e:
                return False
            if self.request in r:
                ret = self.request.recv(1024)
                # Discard anything read, if input is eof, it has
                # disconnected.
                if not ret:
                    return False


class LogStreamerServer(streamer_utils.CustomThreadingTCPServer):

    def __init__(self, *args, **kwargs):
        self.jobdir_root = kwargs.pop('jobdir_root')
        super(LogStreamerServer, self).__init__(*args, **kwargs)


class LogStreamer(object):
    '''
    Class implementing log streaming over the finger daemon port.
    '''

    def __init__(self, host, port, jobdir_root):
        self.log = logging.getLogger('zuul.log_streamer')
        self.log.debug("LogStreamer starting on port %s", port)
        self.server = LogStreamerServer((host, port),
                                        RequestHandler,
                                        jobdir_root=jobdir_root)

        # We start the actual serving within a thread so we can return to
        # the owner.
        self.thd = threading.Thread(target=self._run)
        self.thd.daemon = True
        self.thd.start()

    def _run(self):
        try:
            self.server.serve_forever()
        except Exception:
            self.log.exception("Abnormal termination:")
            raise

    def stop(self):
        if self.thd.is_alive():
            self.server.shutdown()
            self.server.server_close()
            self.thd.join()
            self.log.debug("LogStreamer stopped")
