#!/usr/bin/python3 -u
# pylint: disable=missing-module-docstring,invalid-name
#
# 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


# pylint: disable-next=invalid-name
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)


# pylint: disable-next=wrong-import-position
import csv
# pylint: disable-next=wrong-import-position
from optparse import Option, OptionParser
# pylint: disable-next=wrong-import-position
import re
# pylint: disable-next=wrong-import-position
import errno


sys.path.append("/usr/share/spacewalk")
# pylint: disable-next=wrong-import-position
import reports

try:
    # pylint: disable-next=ungrouped-imports
    from spacewalk.server import rhnSQL
except KeyboardInterrupt:
    systemExit(-1, "\nUser interrupted process.")


# pylint: disable-next=invalid-name,redefined-outer-name
def getClause(clause, word):
    """get clause and column from swith"""
    # pylint: disable-next=consider-using-f-string
    prefix = "--%s-" % clause
    # pylint: disable-next=consider-using-f-string
    assert word.startswith(prefix), "%s is not prefix of %s" % (prefix, word)
    return clause, word[len(prefix) :]


# pylint: disable-next=invalid-name
def getDataDir(reportdb):
    if reportdb:
        return "data"
    else:
        return "legacy"


# pylint: disable-next=invalid-name
def getDatabaseType(reportdb):
    if reportdb:
        return "reporting database"
    else:
        return "legacy"


# pylint: disable-next=invalid-name
def processCommandline(argv):
    """process the commandline"""
    # pylint: disable-next=invalid-name
    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",
        ),
    ]

    # pylint: disable-next=invalid-name
    optionParser = OptionParser(
        # pylint: disable-next=consider-using-f-string
        usage="usage: %s [options] [report_name]" % sys.argv[0],
        option_list=optionsTable,
    )

    i = 0
    unused = []

    # where[column][clause].append(value)
    # pylint: disable-next=redefined-outer-name
    where = {}

    while i < len(argv):
        # pylint: disable-next=redefined-outer-name
        clause = None
        # pylint: disable-next=redefined-outer-name
        column = None
        rest = None
        # pylint: disable-next=redefined-outer-name
        for x in ["where", "ne-where", "le-where", "ge-where", "like"]:
            # pylint: disable-next=consider-using-f-string
            if argv[i].startswith("--%s-" % x):
                clause, rest = getClause(x, argv[i])
                break
            # pylint: disable-next=consider-using-f-string,consider-using-f-string
            elif argv[i] == "--%s" % x or argv[i].startswith("--%s=" % x):
                # pylint: disable-next=consider-using-f-string
                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):
                # pylint: disable-next=consider-using-f-string
                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

    # pylint: disable-next=redefined-outer-name
    options, args = optionParser.parse_args(unused)

    sys.argv[1:] = args

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

    return options, where


# pylint: disable-next=invalid-name,redefined-outer-name
def __field_str(v):
    if not isinstance(v, memoryview):
        return v
    try:
        return v.tobytes().decode()
    # pylint: disable-next=bare-except
    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

    # pylint: disable-next=invalid-name
    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.
                    # pylint: disable-next=consider-using-f-string
                    systemExit(-4, "Unknown report [%s]." % report_name)

                # Check if the other type of report is available
                options.use_reportdb = not options.use_reportdb
                # pylint: disable-next=invalid-name
                dataDir = getDataDir(options.use_reportdb)

                try:
                    report = reports.report(report_name, dataDir, params)
                    sys.stderr.write(
                        # pylint: disable-next=consider-using-f-string
                        "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:
                    # pylint: disable-next=consider-using-f-string
                    systemExit(-4, "Unknown report [%s]." % report_name)

            need_exit = None
            if options.info:
                if report.synopsis is not None:
                    print(report.synopsis)
                else:
                    # pylint: disable-next=consider-using-f-string
                    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:
                        # pylint: disable-next=consider-using-f-string
                        text = "    %s" % c
                    if options.listfieldsinfo and c in report.column_descriptions:
                        # pylint: disable-next=consider-using-f-string
                        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
            # pylint: disable-next=consider-using-dict-items
            for column in where:
                if column not in report.columns:
                    systemExit(
                        -6,
                        # pylint: disable-next=consider-using-f-string
                        "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,
                            # pylint: disable-next=consider-using-f-string
                            "Column [%s] in report [%s] only accepts integer value."
                            % (column, report_name),
                        )

                for clause, values in where[column].items():
                    l = []
                    for v in values:
                        # pylint: disable-next=consider-using-f-string
                        l.append(":p%d" % pi)
                        # pylint: disable-next=consider-using-f-string
                        the_dict_where["p%d" % pi] = v
                        pi += 1

                    # Column named "group" can be a little complicated...
                    if column.lower() == "group":
                        # pylint: disable-next=consider-using-f-string
                        column = '"%s"' % column.lower()

                    if clause == "where":
                        # pylint: disable-next=consider-using-f-string
                        conjunct = "%s in ( %s )" % (column, ", ".join(l))
                    elif clause == "ne-where":
                        # pylint: disable-next=consider-using-f-string
                        conjunct = "%s not in ( %s )" % (column, ", ".join(l))
                    elif clause == "le-where":
                        # pylint: disable-next=consider-using-f-string
                        conjunct = " and ".join("%s <= %s" % (column, v) for v in l)
                    elif clause == "ge-where":
                        # pylint: disable-next=consider-using-f-string
                        conjunct = " and ".join("%s >= %s" % (column, v) for v in l)
                    elif clause == "like":
                        # pylint: disable-next=consider-using-f-string
                        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(
                    # pylint: disable-next=consider-using-f-string
                    "-- 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,
                    # pylint: disable-next=consider-using-f-string
                    "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:
                # pylint: disable-next=unnecessary-lambda
                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:
                        # pylint: disable-next=unsubscriptable-object
                        if prevrow[m] != row[m]:
                            writer.writerow(outrow)
                            outrow = None
                            break

                if outrow is not None:
                    # pylint: disable-next=consider-iterating-dictionary
                    for m in report.multival_columns_reverted.keys():
                        # pylint: disable-next=unsubscriptable-object
                        if prevrow[m] != row[m]:
                            if m not in multival_dupes:
                                multival_dupes[m] = {}
                                # store the dupe value from previous row
                                # pylint: disable-next=unsubscriptable-object
                                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:
                        # pylint: disable-next=no-value-for-parameter,no-value-for-parameter
                        report = reports.report(report_name)
                        synopsis = report.synopsis
                    # pylint: disable-next=bare-except
                    except:
                        # pylint: disable-next=pointless-statement
                        None
                    # pylint: disable-next=consider-using-f-string
                    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
