#!/usr/bin/python3 -u
#
# Copyright (c) 2008--2015 Red Hat, Inc.
#
# This software is licensed to you under the GNU General Public License,
# version 2 (GPLv2). There is NO WARRANTY for this software, express or
# implied, including the implied warranties of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 # along with this software; if not, see
# http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
#
# Red Hat trademarks are not licensed under GPLv2. No permission is
# granted to use or replicate Red Hat trademarks that are incorporated
# in this software or its documentation.
#

import sys
from spacewalk.common.rhnConfig import CFG, initCFG


def systemExit(code, msgs=None):
    """ exit with message and code """
    if msgs:
        if type(msgs) not in [type([]), type(())]:
            msgs = (msgs, )
        for msg in msgs:
            sys.stderr.write(str(msg) + '\n')
    sys.exit(code)

import csv
from optparse import Option, OptionParser
from salt.ext import six
import re
import errno


sys.path.append('/usr/share/spacewalk')
import reports

try:
    from spacewalk.server import rhnSQL
except KeyboardInterrupt:
    systemExit(-1, "\nUser interrupted process.")


def getClause(clause, word):
    """ get clause and column from swith """
    prefix = "--%s-" % clause
    assert word.startswith(prefix), "%s is not prefix of %s" % (prefix, word)
    return clause, word[len(prefix):]

def getDataDir(reportdb):
    if reportdb:
        return "data"
    else:
        return "legacy"

def getDatabaseType(reportdb):
    if reportdb:
        return "reporting database"
    else:
        return "legacy"

def processCommandline(argv):
    """ process the commandline """
    optionsTable = [
        Option('--multival-on-rows', action='store_true', dest='multivalonrows',
            help='if there are multiple values for certain field, repeat the rows'),
        Option('--multival-separator', action='store', dest='multivalseparator',
            default=';',
            help='if there are multiple values for certain field, separate them with this string' \
                ' (unless --multival-on-rows)'),
        Option('--info', action='store_true',
            help='print synopsis of the report in the list of report, or description of individual reports specified'),
        Option('--list-fields', action='store_true',
            dest='listfields',
            help='list fields of the report instead of running the report'),
        Option('--list-fields-info', action='store_true',
            dest='listfieldsinfo',
            help='as --list-fields but also prints description for each column'),
        # This '<column-id>' is here just so optparse can generate nice help message,
        # parsing logic is couple of lines lower...
        Option('--where-<column-id>', action='store', metavar='VALUE',
            dest='_where_trap',
            help='limit the output to records where column-id has value VALUE'),
        Option('--ne-where-<column-id>', action='store', metavar='VALUE',
            dest='_where_trap',
            help='limit the output to records where column-id has value not equal to VALUE'),
        Option('--le-where-<column-id>', action='store', metavar='VALUE',
            dest='_where_trap',
            help='limit the output to records where column-id has value less than or equal to VALUE'),
        Option('--ge-where-<column-id>', action='store', metavar='VALUE',
            dest='_where_trap',
            help='limit the output to records where column-id has value greater than or equal to VALUE'),
        Option('--like-<column-id>', action='store', metavar='VALUE',
            dest='_like_trap',
            help='limit the output to records where column-id has value like VALUE (string only)'),
        Option('-p', '--param', action='append', metavar='PARAM=VALUE',
            dest='params',
            help='Parameter value to pass to the report, overriding the default.'),
        Option('--timezone', action='store', type='str',
            dest='timezone',
            help='set timezone for all dates reported to custom one instead of UTC'),
        Option('--legacy-report', action='store_false', dest='use_reportdb', default=True,
               help='use the legacy SQL statement instead of the reporting database'),
    ]

    optionParser = OptionParser(
        usage="usage: %s [options] [report_name]" % sys.argv[0],
        option_list=optionsTable)

    i = 0
    unused = []

    # where[column][clause].append(value)
    where = {}

    while i < len(argv):
        clause = None
        column = None
        rest = None
        for x in ['where', 'ne-where', 'le-where', 'ge-where', 'like']:
            if argv[i].startswith("--%s-" % x):
                clause, rest = getClause(x, argv[i])
                break
            elif argv[i] == "--%s" % x or argv[i].startswith("--%s=" % x):
                optionParser.error('no such option: %s' % argv[i])
        else:
            unused.append(argv[i])
            i += 1
            continue
        try:
            column, value = rest.split('=', 1)
        except ValueError:
            if i + 1 >= len(argv):
                optionParser.error('option %s has to have a parameter' % argv[i])
            column = rest
            value = argv[i + 1]
            i += 1
        if column == '':
            optionParser.error('<column-id> is required')
        if column == '<column-id>':
            optionParser.error('use actual column-id in the --where-<column-id> parameter')

        column = column.replace('-', '_')

        # where[column][clause].append(value)
        where.setdefault(column, {}).setdefault(clause, []).append(value)

        i += 1

    options, args = optionParser.parse_args(unused)

    sys.argv[1:] = args

    if not options.timezone:
        options.timezone = 'UTC'

    return options, where


def __field_str(v):
    if not isinstance(v, memoryview):
        return v
    try:
        return v.tobytes().decode()
    except:
        return '<binary data>'
    return v


if __name__ == '__main__':
    options, where = processCommandline(sys.argv[1:])
    initCFG('server.satellite')

    reportdb_configured = not CFG.REPORT_DB_NAME is None

    if not reportdb_configured and options.use_reportdb:
        sys.stderr.write("Warning: Reporting database is not configured. Fallback to legacy reports\n")
        options.use_reportdb = False

    dataDir = getDataDir(options.use_reportdb)

    try:
        if len(sys.argv) > 2:
            systemExit(-5, 'Only one report name expected.')
        if len(sys.argv) > 1:
            report_name = sys.argv[1]

            # Convert the parameters into a dict
            params = {}
            if options.params is not None:
                for param_value in options.params:
                    matcher = re.match("^([^=]+)=([^=]+)$", param_value)
                    if not matcher:
                        systemExit(-5, 'Invalid parameter value: ' + param_value)
                    params[matcher.group(1)] = matcher.group(2)

            try:
                report = reports.report(report_name, dataDir, params)
            except(reports.spacewalk_unknown_report):
                if not reportdb_configured:
                    # if reportdb is not configured, we have already looked for the legacy report and it's missing.
                    # This means we do not have an alternative.
                    systemExit(-4, 'Unknown report [%s].' % report_name)

                # Check if the other type of report is available
                options.use_reportdb = not options.use_reportdb
                dataDir = getDataDir(options.use_reportdb)

                try:
                    report = reports.report(report_name, dataDir, params)
                    sys.stderr.write("Warning: %s does not exists as a %s report. Using the %s one\n" % (report_name, getDatabaseType(not options.use_reportdb), getDatabaseType(options.use_reportdb)))
                except(reports.spacewalk_unknown_report):
                    systemExit(-4, 'Unknown report [%s].' % report_name)

            need_exit = None
            if options.info:
                if report.synopsis is not None:
                    print(report.synopsis)
                else:
                    print("No synopsis for report %s." % report_name)
                if report.description is not None:
                    print()
                    print(report.description)
                need_exit = True

            if options.listfields or options.listfieldsinfo:
                if options.info:
                    print()
                    print("Fields in the report:")
                    print()

                for c in report.columns:
                    text = c
                    if options.info:
                        text = "    %s" % c
                    if options.listfieldsinfo and c in report.column_descriptions:
                        text = "%s: %s" % (text, report.column_descriptions[c])
                    print(text)
                need_exit = True

            if need_exit:
                sys.exit(0)

            the_sql_where = []
            the_dict_where = {}
            pi = 1
            for column in where:
                if column not in report.columns:
                    systemExit(-6, 'Unknown column [%s] in report [%s].' % (column, report_name))
                for v in (val for vals in where[column].values() for val in vals):
                    if report.column_types[column] == 'i' and not re.match('^[0-9]+$', v):
                        systemExit(-7, 'Column [%s] in report [%s] only accepts integer value.' % (column, report_name))

                for clause, values in six.iteritems(where[column]):
                    l = []
                    for v in values:
                        l.append(':p%d' % pi)
                        the_dict_where['p%d' % pi] = v
                        pi += 1

                    # Column named "group" can be a little complicated...
                    if column.lower() == 'group':
                       column = '"%s"' % column.lower()

                    if clause == 'where':
                        conjunct = '%s in ( %s )' % (column, ", ".join(l))
                    elif clause == 'ne-where':
                        conjunct = '%s not in ( %s )' % (column, ", ".join(l))
                    elif clause == 'le-where':
                        conjunct = " and ".join('%s <= %s' % (column, v) for v in l)
                    elif clause == 'ge-where':
                        conjunct = " and ".join('%s >= %s' % (column, v) for v in l)
                    elif clause == 'like':
                        conjunct = " and ".join('%s like %s' % (column, v) for v in l)
                    else:
                        assert False, "Unsupported clause"

                    the_sql_where.append(conjunct)

            rhnSQL.initDB(reportdb=options.use_reportdb)

            writer = csv.writer(sys.stdout, lineterminator="\n")

            the_sql = report.sql

            if the_sql_where:
                the_sql = the_sql.replace('-- where placeholder', 'where %s' % ' and '.join(the_sql_where))

            tz = rhnSQL.prepare('set session timezone to :tz')
            tz.execute(tz=options.timezone)

            h = rhnSQL.prepare(the_sql)
            h.execute(**dict(tuple(report.params.items()) + tuple(the_dict_where.items())))

            db_columns = [x[0].lower() for x in h.description]
            if db_columns != report.columns:
                systemExit(-3,
                    "Columns in report spec and in the database do not match:\nexpected %s\n     got %s" % (report.columns, db_columns))
            writer.writerow(report.columns)

            row = h.fetchone()
            prevrow = None
            outrow = None
            multival_dupes = {}
            while row is not None:
                row = list(map(lambda v: __field_str(v), row))
                if options.multivalonrows or not report.multival_column_names.keys():
                    writer.writerow(row)
                    row = h.fetchone()
                    continue

                if outrow is not None:
                    for m in report.multival_columns_stop:
                        if prevrow[m] != row[m]:
                            writer.writerow(outrow)
                            outrow = None
                            break

                if outrow is not None:
                    for m in report.multival_columns_reverted.keys():
                        if prevrow[m] != row[m]:
                            if m not in multival_dupes:
                                multival_dupes[m] = {}
                                # store the dupe value from previous row
                                multival_dupes[m][prevrow[m]] = 1
                            if not row[m] in multival_dupes[m]:
                                outrow[m] = str(outrow[m]) + options.multivalseparator + str(row[m])
                                multival_dupes[m][row[m]] = 1
                            else:
                                # check another multival
                                continue

                if outrow is None:
                    outrow = []
                    for x in row:
                        if x is None:
                            outrow.append(None)
                        else:
                            outrow.append(str(x))
                    multival_dupes = {}

                prevrow = row
                row = h.fetchone()

            if outrow is not None:
                writer.writerow(outrow)
        else:
            for report_name in sorted(reports.available_reports(dataDir)):
                if options.info:
                    synopsis = ''
                    try:
                        report = reports.report(report_name)
                        synopsis = report.synopsis
                    except:
                        None
                    print("%s: %s" % (report_name, synopsis))
                else:
                    print(report_name)

    except KeyboardInterrupt:
        systemExit(-1, "\nUser interrupted process.")
    except (rhnSQL.SQLError, rhnSQL.SQLSchemaError, rhnSQL.SQLConnectError) as e:
        # really a stub for better exception handling in the future.
        sys.stderr.write("SQL error occurred, traceback follows...\n")
        raise
    except IOError as e:
        if e.errno == errno.EPIPE:
            sys.exit(0)
        else:
            raise

