''' Convenience class for writing cron scripts'''
# pylint: disable=R0903

# Copyright 2014 42Lines, Inc.
# Original Author: Jim Browne
#
# 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 datetime as DT
from lockfile import FileLock, LockFailed, LockTimeout
import logging
import logging.handlers
import __main__ as main
from optparse import OptionParser, make_option
import os
from random import randint
import sys
import time

# Support for RotateFileHandler in multiple processes
from multiprocessinglog import MultiProcessingLog, MultiProcessingLogStream

__version__ = '0.2.1'


class StdErrFilter(logging.Filter):
    ''' Discard all events below a configured level '''

    def __init__(self, level=logging.WARNING, discard_all=False):
        self.level = level
        self.discard_all = discard_all
        super(StdErrFilter, self).__init__()

    def filter(self, record):
        if self.discard_all:
            return False
        else:
            return (record.levelno >= self.level)


class CronScript(object):
    ''' Convenience class for writing cron scripts '''

    def __init__(self, args=None, options=None, usage=None,
                 disable_interspersed_args=False):
        self.lock = None
        self.start_time = None
        self.end_time = None

        if options is None:
            options = []

        if args is None:
            args = sys.argv[1:]

        prog = os.path.basename(main.__file__)
        logfile = os.path.join('/var/log/', "%s.log" % prog)
        lockfile = os.path.join('/var/lock/', "%s" % prog)
        stampfile = os.path.join('/var/tmp/', "%s.success" % prog)
        options.append(make_option("--debug", "-d", action="store_true",
                                   help="Minimum log level of DEBUG"))
        options.append(make_option("--quiet", "-q", action="store_true",
                                   help="Only WARN and above to stdout"))
        options.append(make_option("--nolog", action="store_true",
                                   help="Do not log to LOGFILE"))
        options.append(make_option("--logfile", type="string",
                                   default=logfile,
                                   help="File to log to, default %default"))
        options.append(make_option("--syslog", action="store_true",
                                   help="Log to syslog instead of a file"))
        options.append(make_option("--nolock", action="store_true",
                                   help="Do not use a lockfile"))
        options.append(make_option("--lockfile", type="string",
                                   default=lockfile,
                                   help="Lock file, default %default"))
        options.append(make_option("--nostamp", action="store_true",
                                   help="Do not use a success stamp file"))
        options.append(make_option("--stampfile", type="string",
                                   default=stampfile,
                                   help="Success stamp file, default %default"))
        helpmsg = "Lock timeout in seconds, default %default"
        options.append(make_option("--locktimeout", default=90, type="int",
                                   help=helpmsg))
        helpmsg = "Sleep a random time between 0 and N seconds before starting, default %default"
        options.append(make_option("--splay", default=0, type="int",
                                   help=helpmsg))

        parser = OptionParser(option_list=options, usage=usage)
        if disable_interspersed_args:
            # Stop option parsing at first non-option
            parser.disable_interspersed_args()
        (self.options, self.args) = parser.parse_args(args)

        self.logger = logging.getLogger(main.__name__)

        if self.options.debug:
            self.logger.setLevel(logging.DEBUG)
        else:
            self.logger.setLevel(logging.INFO)

        # Log to syslog
        if self.options.syslog:
            syslog_formatter = logging.Formatter("%s: %%(levelname)s %%(message)s" % prog)
            handler = logging.handlers.SysLogHandler(
                    address="/dev/log",
                    facility=logging.handlers.SysLogHandler.LOG_LOCAL3
                    )
            handler.setFormatter(syslog_formatter)
            self.logger.addHandler(handler)

        default_formatter = logging.Formatter("%(asctime)s;%(levelname)s;%(message)s",
                                              "%Y-%m-%d-%H:%M:%S")
        if not self.options.nolog:
            # Log to file
            try:
                handler = MultiProcessingLog(
                    "%s" % (self.options.logfile),
                    maxBytes=(50 * 1024 * 1024),
                    backupCount=10)
            except IOError:
                sys.stderr.write("Fatal: Could not open log file: %s\n"
                                 % self.options.logfile)
                sys.exit(1)

            handler.setFormatter(default_formatter)
            self.logger.addHandler(handler)

        # If quiet, only WARNING and above go to STDERR; otherwise all
        # logging goes to stderr
        handler2 = MultiProcessingLogStream(sys.stderr)
        if self.options.quiet:
            err_filter = StdErrFilter()
            handler2.addFilter(err_filter)
        handler2.setFormatter(default_formatter)
        self.logger.addHandler(handler2)

        self.logger.debug(self.options)

    def __enter__(self):
        if self.options.splay > 0:
            splay = randint(0, self.options.splay)
            self.logger.debug('Sleeping for %d seconds (splay=%d)' %
                              (splay, self.options.splay))
            time.sleep(splay)
        self.start_time = DT.datetime.today()
        if not self.options.nolock:
            self.logger.debug('Attempting to acquire lock %s (timeout %s)',
                              self.options.lockfile,
                              self.options.locktimeout)
            self.lock = FileLock(self.options.lockfile)
            try:
                self.lock.acquire(timeout=self.options.locktimeout)
            except LockFailed as e:
                self.logger.error("Lock could not be acquired.")
                self.logger.error(str(e))
                sys.exit(1)
            except LockTimeout as e:
                msg = "Lock could not be acquired. Timeout exceeded."
                self.logger.error(msg)
                sys.exit(1)

    def __exit__(self, etype, value, traceback):
        self.end_time = DT.datetime.today()
        self.logger.debug('Run time: %s', self.end_time - self.start_time)
        if not self.options.nolock:
            self.logger.debug('Attempting to release lock %s',
                              self.options.lockfile)
            self.lock.release()
        if etype is None:
            if not self.options.nostamp:
                open(self.options.stampfile, "w")