#!/usr/bin/env python2
# -*- coding: utf-8 -*-
#
#    LinOTP - the open source solution for two factor authentication
#    Copyright (C) 2010 - 2019 KeyIdentity GmbH
#
#    This file is part of LinOTP server.
#
#    This program is free software: you can redistribute it and/or
#    modify it under the terms of the GNU Affero General Public
#    License, version 3, as published by the Free Software Foundation.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU Affero General Public License for more details.
#
#    You should have received a copy of the
#               GNU Affero General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
#
#    E-mail: linotp@keyidentity.com
#    Contact: www.linotp.org
#    Support: www.keyidentity.com
#

""" This is a janitor program, that cleans up the audit log
    If the audit entries exceed the linotpAudit.sql.highwatermark
    the tool will delete old entries and only leave the
       linotpAudit.sql.lowwatermark entries

    14-09-02: added ability to dump the 'to be deleted audit data' into a
              directory. This could be defined by 2 new linotp config
              entries:

            - linotpAudit.janitor.dir = /tmp

              the dumpfile is extend with date and the biggest id of the
              to be deleted data eg:     SQLData.2014.9.2-22382.csv

            - linotpAudit.janitor.logdir = /var/log/linotp/


"""

import os
import sys
import datetime

from sqlalchemy import *
from getopt import getopt, GetoptError

import ConfigParser
try:
    from linotp.lib.utils import INI_FILE
except ImportError:
    INI_FILE = "/etc/linotp2/linotp.ini"


import logging
log = None


def usage():
    print '''
Usage:
    linotp-sql-janitor [--high=###] [--low=###] [-e] [--dir=/path/to/data] [-i=linotp.ini]

    --high=, -h   specify the high watermark (the maximum number of audit entries allowed)
    --low=, -l    specify the low watermark  (the number of entries kept, if the high watermark is exceeded)
    --export, -e  switch, which defines if the data should be exported before they are deleted
    --dir=, -d    the directory, where the exported data should be stored
    --ini=, -i    alternative linotp configuration file

'''
    return


def init_logging(LOGDIR, level=logging.DEBUG):

    global log

    log = logging.getLogger(__name__)
    log.setLevel(level)

    time = datetime.datetime.now()
    LOG_FILE = "SQLJanitor.%s" % (time.strftime("%Y%m%d"))
    log_file_handler = logging.FileHandler(os.path.join(LOGDIR, LOG_FILE))
    log_file_handler.setLevel(logging.DEBUG)
    log_file_formatter = logging.Formatter('%(message)s')
    log_file_handler.setFormatter(log_file_formatter)
    log.addHandler(log_file_handler)

def log_info(msg):
    sys.stdout.write("[SQLJanitor] %s\n" % msg)
    log.info("[SQLJanitor] %s" % msg)

def log_debug(msg):
    sys.stdout.write("[SQLJanitor] %s\n" % msg)
    log.debug("[SQLJanitor] %s" % msg)

def log_error(msg):
    sys.stderr.write("[SQLJanitor] %s\n" % msg)
    log.error("[SQLJanitor] %s" % msg)


def config_get(section, option, default="", ini_file=None):
    """
    get the configuration entry
    """
    if ini_file == None:
        ini_file = INI_FILE

    config = ConfigParser.ConfigParser()
    config.read([ini_file])
    if config.has_option(section, option):
        return config.get(section, option)
    else:
        return default


class SQLJanitor():
    """
    script to help the house keeping of audit entries
    """

    def __init__(self, SQL_URL, export=None):

        self.export_dir = export
        engine = create_engine(SQL_URL)
        engine.echo = False  # We want to see the SQL we're creating
        metadata = MetaData(engine)
        engine = create_engine(SQL_URL)

        engine.echo = False  # We want to see the SQL we're creating
        metadata = MetaData(engine)

        # The audit table already exists, so no need to redefine it. Just
        # load it from the database using the "autoload" feature.
        self.audit = Table('audit', metadata, autoload=True)


    def export_data(self, max_id):
        """
        export each audit row into a csv output

        :param max_id: all entries with lower id will be dumped
        :return: - nothing -
        """

        if not self.export_dir:
            log_info('no export directory defined')
            return

        if not os.path.isdir(self.export_dir):
            log_error('export directory %r not found' % self.export_dir)
            return

        # create the filename
        t2 = datetime.datetime.now()
        filename = "SQLData.%d.%d.%d-%d.csv" % (t2.year, t2.month, t2.day, max_id)

        f = None
        try:
            f = open(os.path.join(self.export_dir, filename), "w")

            s = self.audit.select(self.audit.c.id < max_id).order_by(desc(self.audit.c.id))
            result = s.execute()

            # write the csv header
            keys = result.keys()
            prin = "; ".join(keys)
            f.write(prin)
            f.write("\n")

            for row in result:
                row_data = []
                vals = row.values()
                for val in vals:
                    if type(val) in [int, long]:
                        row_data.append("%d" % val)
                    elif type(val) in [str, unicode]:
                        row_data.append('"%s"' % val)
                    elif val is None:
                        row_data.append(" ")
                    else:
                        row_data.append("?")
                        log_error('exporting of unknown data / data type %r' % val)
                prin = "; ".join(row_data)
                f.write(prin)
                f.write("\n")
        except Exception as exx:
            log_error('failed to export data %r' % exx)
            raise exx
        finally:
            if f:
                f.close()
        return

    def cleanup(self, SQL_HIGH, SQL_LOW):
        """
        identify the audit data and delete them

        :param SQL_HIGH: the maximum amount of data
        :param SQL_LOW: the minimum amount of data that should not be deleted

        :return: - nothing -
        """
        t1 = datetime.datetime.now()
        id_pos = 0
        overall_number = 0

        rows = self.audit.count().execute()
        row = rows.fetchone()
        overall_number = int(row[id_pos])


        log_info("Found %i entries in the audit" % overall_number)
        if overall_number >= SQL_HIGH:

            log_info("Deleting older entries")
            s = self.audit.select().order_by(asc(self.audit.c.id)).limit(1)
            rows = s.execute()
            first_id = int(rows.fetchone()[id_pos])

            s = self.audit.select().order_by(desc(self.audit.c.id)).limit(1)
            rows = s.execute()
            last_id = int (rows.fetchone()[id_pos])

            log_info("Found ids between %i and %i" % (first_id, last_id))
            delete_from = last_id - SQL_LOW

            if delete_from > 0:
                # if export is enabled, we start the export now
                self.export_data(delete_from)

                log_info("Deleting all IDs less than %i" % delete_from)
                s = self.audit.delete(self.audit.c.id < delete_from)
                s.execute()

            else:
                log_info("Nothing to do. "
                        "There are less entries than the low watermark")

        else:
            log_info("Nothing to be done: %i below high watermark %i" %
                (overall_number, SQL_HIGH))


        t2 = datetime.datetime.now()

        duration = t2 - t1
        log_info("Took me %i seconds" % duration.seconds)
        return




def main():

    try:
        opts, args = getopt(sys.argv[1:], "eh:l:i:d:",
                ["export", "high=", "low=", "ini=", "dir="])

    except GetoptError:
        print "There is an error in your parameter syntax:"
        usage()
        sys.exit(1)


    SQL_HIGH = None
    SQL_LOW = None
    export_csv = False
    export_dir = None
    ini_file = INI_FILE

    for opt, arg in opts:
        if opt in ('--high, -h'):
            SQL_HIGH = int(arg)
        elif opt in ('--low, -l'):
            SQL_LOW = int(arg)
        elif opt in ('--export', '-e'):
            export_csv = True
        elif opt in ('--dir', '-d'):
            export_dir = arg
        elif opt in ("--ini", "-i"):
            ini_file = arg

    if not os.path.isfile(ini_file):
        sys.stderr.write('ini file could not be found: %s' % arg)
        sys.exit(-1)

    # get default values from config file
    SQL_URL = config_get(section="DEFAULT",
                         option="linotpAudit.sql.url",
                         ini_file=ini_file)

    LOGDIR = config_get(section="DEFAULT",
                         option="linotpAudit.janitor.logdir",
                         default="/var/log/linotp/",
                         ini_file=ini_file)

    if not SQL_HIGH:
        SQL_HIGH = int(config_get(section="DEFAULT",
                                  option="linotpAudit.sql.highwatermark",
                                  default="10000",
                                  ini_file=ini_file))

    if not SQL_LOW:
        SQL_LOW = int(config_get(section="DEFAULT",
                                 option="linotpAudit.sql.lowwatermark",
                                 default="5000",
                                 ini_file=ini_file))

    SQL_EXPORT = None
    if export_csv:
        SQL_EXPORT = config_get(section="DEFAULT",
                                option="linotpAudit.janitor.dir",
                                default="/tmp/",
                                ini_file=ini_file)

    if export_dir and os.path.isdir(export_dir):
        SQL_EXPORT = export_dir


    init_logging(LOGDIR, logging.INFO)

    sqljanitor = SQLJanitor(SQL_URL, export=SQL_EXPORT)
    sqljanitor.cleanup(SQL_HIGH, SQL_LOW)

    sys.exit(0)

if __name__ == '__main__':
    main()
