#!/opt/cloudlinux/venv/bin/python3 -bb
# coding=utf-8
#
# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2020 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENCE.TXT
#

import getopt
import os
import random
import sys

from sentry import init_wmt_sentry_client, setup_logger

__ALL__ = ["add_cron", "erase_cron", "remove_cron", "add_cron_task"]

WMT_CRONS = ['wmt-clickhouse-reporter', 'wmt-email-reporter']


def usage():
    print('')
    print('Use following syntax to manage WMT cron jobs install utility:')
    print(sys.argv[0] + " [OPTIONS]")
    print('Options:')
    print(" -i | --install     : install wmt cron jobs")
    print(" -d | --delete     : delete  wmt cron jobs")
    print(" -u | --update     : update  wmt cron jobs")


def add_cron(file_name, minute, hour, day, month, day_of_week, user, command,
             check_command=True):
    """
    Add new cron task into crontab schedule if this task or command wasn't already existed in the cron-file.

    :param str file_name: Name of cron-file in /etc/cron.d
    :param minute: Integer or char 'r' if to set random minute
    :param hour: Integer or char 'r' if to set random hour
    :param int, str day: Day number
    :param int, str month: Month number
    :param int, str day_of_week: Day of week number
    :param str user: Under what user do run command
    :param str command: What command do run
    :param bool check_command: If it is False, check that whole cron-task line already exists in crontab,
        check that command string exists instead. Default is True, check a command string.
    """
    if minute == 'r':
        minute = int(round(random.uniform(0, 59)))  # pylint: disable=round-builtin
    if hour == 'r':
        hour = int(round(random.uniform(0, 23)))  # pylint: disable=round-builtin
    try:
        cron_task = format_cron_task(minute, hour, day, month, day_of_week, user, command)
        add_cron_task(file_name, cron_task, check_command)
    except TypeError:
        sys.stderr.write("Can not add task with wrong syntax")


def add_cron_task(file_name, task, check_command=False):
    """
    Add new cron task in cron format if this task or command in this task wasn't already existed in the cron-file.

    :param str file_name: Name of cron-file in /etc/cron.d
    :param str task: Cron task in format "min hour day mon d_of_w user command"
    :param bool check_command: If it is False, check that whole cron-task line already exists in crontab,
        check that command string exists instead. Default is False, check a whole cron-task string.
    """
    cron_file_path = os.path.join('/etc/cron.d/', file_name)
    try:
        content = ''
        if os.path.exists(cron_file_path):
            with open(cron_file_path) as f:
                content = f.readlines()
        if not is_in_cron(task, content, check_command):
            with open(cron_file_path, 'a') as f:
                f.write("%s\n" % task)
    except (IOError, OSError):
        return False
    return True


def remove_cron(file_name):
    """
    Remove cron-file from fs

    :param str file_name: Name of cron-file in /etc/cron.d
    """
    try:
        os.remove(
            os.path.join('/etc/cron.d/', file_name)
        )
    except (OSError, IOError):
        pass


def erase_cron(file_name):
    """
    Make cron-file empty

    :param str file_name: Name of cron-file in /etc/cron.d
    """
    f = None
    try:
        f = open(
            os.path.join("/etc/cron.d/", file_name), "w"
        )
    except (IOError, OSError) as err:
        sys.stderr.write("Can not erase crontab file %s because %s\n" % (
            file_name, str(err)))
    else:
        f.close()


def format_cron_task(minute, hour, day, month, day_of_week, user, command):
    """
    Build cron-task string in the cron format

    :param minute: Integer or char 'r' if to set random minute
    :param hour: Integer or char 'r' if to set random hour
    :param int day: Day number
    :param int month: Month number
    :param int day_of_week: Day of week number
    :param str user: Under what user do run command
    :param str command: What command do run
    :return: Cron-task in the cron format
    :rtype: str
    """
    arguments = (minute, hour, day, month, day_of_week, user, command)
    for arg in arguments:
        if arg is None:
            raise TypeError("Wrong schedule for cron task")
    return "%2s %2s %2s %2s %2s %10s %s" % arguments


def parse_cron_task(task):
    """
    Split cron task string into cron task parts

    :param str task: Cron-task string in the cron format
    :return: List of cron-task parts
    :rtype: list of str
    """
    return task.split(None, 6)


def get_task_in_cron(crontab, get_parsed=False):
    """
    Returns iterator through crontab tasks

    :param iterable crontab: Iterator with crontab tasks' strings
    :param bool get_parsed: If it is True, return crontab task as list of task's parts
        return crontab task as a string instead
    :return: Crontab task
    :rtype: str
    :rtype: list of str
    """
    for cron_t in (s.strip() for s in crontab):
        try:
            if get_parsed:
                t = parse_cron_task(cron_t)
            else:
                t = format_cron_task(*parse_cron_task(cron_t))
        except TypeError:
            sys.stderr.write("Wrong crontab task syntax: %s\n" % cron_t)
        else:
            yield t


def is_task_in_cron(task, cron_content):
    """
    Find first occurence of task in cron-file if it has

    :param str task: Cron-task in cront format to compare with
    :param list cron_content: list of cron contents lines
    :return: True if such a task is already existed in cron-file, False instead
    :rtype: bool
    """
    for t in get_task_in_cron(cron_content):
        if t == task:
            return True
    return False


def is_command_in_cron(task, cron_content):
    """
    :param str task: Task with command to looking for
    :param list cron_content: list of cron content lines
    :return: True if such a command is already existed in cron-file, False instead
    :rtype: bool

    Find first occurence of command in cron-file if it has
    """
    command = parse_cron_task(task)[-1]
    for t in get_task_in_cron(cron_content, get_parsed=True):
        if t[-1] == command:
            return True
    return False


def is_in_cron(task, cron_content, check_command=False):
    """
    Find first occurence of command or task in cron-file if it has

    :param str task: Task or command to looking for
    :param list cron_content: content of cron file
    :param bool check_command: If it is True, check command occurence, check task occurence instead
    :return: True if such a command or task is already existed in cron-file, False instead
    :rtype: bool
    """
    if check_command:
        return is_command_in_cron(task, cron_content)
    return is_task_in_cron(task, cron_content)


def is_valid_cron_task(t):
    """
    Straight-forward approach to cron minute/hour validation
    (our crons always have digits on minute/hour positions)

    Better to clone croniter package for advanced validation
    """
    if not t[0].isdigit() or int(t[0]) >= 60:
        return False
    if not t[1].isdigit() or int(t[1]) >= 24:
        return False
    return True


def get_cron_list():
    return WMT_CRONS


def get_cron_params(cron_name):
    """
    Get crontab entries for WMT cron jobs
    """
    wmt_bin = '/usr/share/web-monitoring-tool/wmtbin'
    wmt_api = os.path.join(wmt_bin, 'wmt-api')
    logfile = '/var/log/cl_wmt.log'

    if cron_name == 'wmt-clickhouse-reporter':
        send_to_clickhouse = f'{wmt_api} --send-clickhouse'

        return [
            'r', 'r', '*', '*', '*', 'root',
            f'/usr/bin/flock -n /var/run/wmt_clickhouse_report.cronlock {send_to_clickhouse} &>> {logfile}'
        ]
    elif cron_name == 'wmt-email-reporter':
        report_to_mail_cmd = f'{wmt_api} --send-email'

        return [
            '0', '0', '*', '*', '*', 'root',
            f'/usr/bin/flock -n /var/run/wmt_email_report.cronlock {report_to_mail_cmd} &>> {logfile}'
        ]
    elif cron_name == 'wmt-file-rotator':
        rotate_wmt_files = '-name "wmt-db-*.sqlite" -delete -or -name "wmt_report*.json" -delete'
        linux_find = f'/usr/bin/find /var/lve/wmt/ -maxdepth 1 -mtime +7 {rotate_wmt_files}'

        return [
            '0', '0', '*', '*', '*', 'root',
            '/usr/bin/flock -n /var/run/wmt_file_rotator.cronlock ' + linux_find
        ]
    else:
        raise ValueError('Invalid cron name: %s', cron_name)


def install_crons():
    for cron_name in get_cron_list():
        add_cron(cron_name, *get_cron_params(cron_name))


def delete_crons():
    for cron_name in get_cron_list():
        remove_cron(cron_name)


def update_crons():
    for cron_name in get_cron_list():
        cron_file_path = os.path.join('/etc/cron.d/', cron_name)
        if not os.path.exists(cron_file_path):
            continue
        # always update cron file with actual tasks
        remove_cron(cron_name)
        add_cron(cron_name, *get_cron_params(cron_name))


if __name__ == "__main__":
    logger = setup_logger('cron_control')
    init_wmt_sentry_client()
    try:
        opts, args = getopt.getopt(
            sys.argv[1:],
            "hidu",
            ["help", "postupcp", "install", "delete", "update"]
        )
    except getopt.GetoptError as err:
        # print help information and exit:
        print(str(err))  # will print something like "option -a not recognized"
        usage()
        sys.exit(2)

    try:
        for o, a in opts:
            if o in ("-h", "--help"):
                usage()
                sys.exit()
            elif o in ("-i", "--install"):
                install_crons()
            elif o in ("-d", "--delete"):
                delete_crons()
            elif o in ("-u", "--update"):
                update_crons()
            else:
                usage()
                sys.exit(2)
    except Exception as e:
        logger.exception(e)
