#!/usr/bin/python3
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

import os
import sys
from argparse import ArgumentParser
from subprocess import call

import dbus
import dbus.lowlevel

import problem

import report
from reportclient import (RETURN_OK,
                          RETURN_FAILURE,
                          RETURN_CANCEL_BY_USER,
                          RETURN_STOP_EVENT_RUN,
                          log1,
                          set_verbosity)

CD_DUMPDIR = "Directory"
FILENAME_PACKAGE = "package"
FILENAME_UID = "uid"
FILENAME_UUID = "uuid"
FILENAME_DUPHASH = "duphash"

def run_event(event_name, dump_dir_name):
    '''
    Run event with `event_name` on problem directory `dump_dir_name`
    '''

    state, ret = report.run_event_on_problem_dir(dump_dir_name, event_name)

    if ret == 0 and state.children_count == 0:
        log1("Didn't find definition of event '%s'", event_name)
        return False

    return True

def run_autoreport(problem_data, event_name):
    """Runs autoreporting event

    Requires CD_DUMPDIR key in problem_data.

    Keyword arguments:
    problem_data -- problem data of notified problems

    Returns None as it raises an exception on error

    Raises:
    KeyError -- if any of required elements is missing
    RuntimeError -- if event run fails
    """

    dir_name = problem_data.get(CD_DUMPDIR)
    if dir_name is None:
        raise KeyError(CD_DUMPDIR)

    log1("Running autoreporting event: '{0}'".format(event_name))

    res, ret  = report.run_event_on_problem_dir(dir_name[0], event_name)

    if res.children_count == 0 and ret == 0:
        raise RuntimeError("No processing is specified for event '{0}'"
                .format(event_name))

    if not ret in [RETURN_OK, RETURN_CANCEL_BY_USER, RETURN_STOP_EVENT_RUN]:
        raise RuntimeError("Event '{0}' exited with {1}"
                .format(event_name, ret))

def emit_crash_dbus_signal(problem_data):
    """Emits a Crash signal on D-Bus Problem bus

    Emits a signal with 5 members:
        package -- value of 'package' element in problem_data
        problem_id -- value of 'Directory' element in problem_data
        uid -- empty string if 'uid' element is not present in problem_data
        uuid -- empty string if 'uuid' element is not present in problem_data
        duphash -- empty string if 'duphash' element is not present in problem_data

    Keyword arguments:
    problem_data -- problem data of notified problems

    Returns None as it raises an exception on error

    Raises:
    RuntimeError -- for all D-Bus related errors
    KeyError -- if any of required elements is missing
    """

    bus = None
    try:
        bus = dbus.SystemBus()
        msg = dbus.lowlevel.SignalMessage("/org/freedesktop/problems",
                "org.freedesktop.problems", "Crash")

        # List of tuples where the first member is element name and the second
        # member is a Boolean flag which is True if the element is required
        arguments = ((FILENAME_PACKAGE, False), (CD_DUMPDIR, True),
                (FILENAME_UID, False), (FILENAME_UUID, False),
                (FILENAME_DUPHASH, False))

        for elem in arguments:
            itm = problem_data.get(elem[0])

            if itm is None:
                if elem[1]:
                    raise KeyError(elem[0])

                msg.append("", signature="s")
            else:
                msg.append(itm[0], signature="s")


        bus.send_message(msg)
    except dbus.exceptions.DBusException as ex:
        raise RuntimeError("Failed to emit D-Bus Crash signal: {0}"
                .format(str(ex)))
    finally:
        if bus is not None:
            bus.close()

def build_notification_problem_data(problem_dir):
    """Loads all necessary problem elements

    Keyword arguments:
    problem_dir -- an absolute file system path problem directory

    Returns an instance of report.problem_data

    Raises:
    ValueError -- if problem_dir is not an absolute path, if problem_dir cannot
        be opened and if any required problem element is missing.
    """

    if not os.path.isabs(problem_dir):
        raise ValueError("problem directory must be absolute path")

    prblm_dt = report.problem_data()

    try:
        dump_dir = report.dd_opendir(problem_dir, report.DD_OPEN_READONLY)
        if not dump_dir:
            raise ValueError("cannot open problem directory")

        dd_load_flag = (report.DD_LOAD_TEXT_RETURN_NULL_ON_FAILURE
                | report.DD_FAIL_QUIETLY_ENOENT)

        pd_add_flag = report.CD_FLAG_TXT | report.CD_FLAG_ISNOTEDITABLE

        package = dump_dir.load_text(FILENAME_PACKAGE, dd_load_flag)
        if package:
            prblm_dt.add(FILENAME_PACKAGE, package, pd_add_flag)
        prblm_dt.add(CD_DUMPDIR, problem_dir, pd_add_flag)

        for element in (FILENAME_UID, FILENAME_UUID, FILENAME_DUPHASH):
            val = dump_dir.load_text(element, dd_load_flag)
            if val is not None:
                prblm_dt.add(element, val, pd_add_flag)
    finally:
        dump_dir.close()

    return prblm_dt


if __name__ == "__main__":
    CMDARGS = ArgumentParser(
            description=("Announce a new or duplicated problem via"
                " all accessible channels"),
            epilog=("Reads the default configuration from 'abrt.conf' file"))
    CMDARGS.add_argument("-d", "--problem-dir",
            type=str, required=True,
            help="An absolute path to a new or duplicated problem directory")
    CMDARGS.add_argument("-v", "--verbose",
            action="count", dest="verbose", default=0,
            help="Be verbose")
    CMDARGS.add_argument("-a", "--autoreporting",
            action="store_true", dest="autoreporting", default=False,
            help="Force to run autoreporting event")
    CMDARGS.add_argument("-e", "--autoreporting-event",
            type=str, dest="autoreporting_event",
            help="Overwrite autoreporting event name")

    OPTIONS = CMDARGS.parse_args()

    DIR_PATH = OPTIONS.problem_dir

    verbose = 0
    ABRT_VERBOSE = os.getenv("ABRT_VERBOSE")
    if ABRT_VERBOSE:
        try:
            verbose = int(ABRT_VERBOSE)
        except:
            pass

    verbose += OPTIONS.verbose
    set_verbosity(verbose)
    os.environ["ABRT_VERBOSE"] = str(verbose)

    try:
        conf = problem.load_conf_file("abrt.conf")
    except OSError as ex:
        sys.stderr.write("{0}".format(str(ex)))
        sys.exit(RETURN_FAILURE)

    try:
        PD = build_notification_problem_data(DIR_PATH)
    except ValueError as ex:
        sys.stderr.write("Cannot notify '{0}': {1}\n".
                format(DIR_PATH, str(ex)))
        sys.exit(RETURN_FAILURE)

    # The execution must continue because we should try to notify via all
    # configured channels. One of them might work properly.
    return_status = RETURN_OK
    try:
        emit_crash_dbus_signal(PD)
    except RuntimeError as ex:
        sys.stderr.write("Cannot notify '{0}' via D-Bus: {1}\n".
                format(DIR_PATH, str(ex)))
        return_status = RETURN_FAILURE
    except KeyError as ex:
        # this is a bug in build_notification_problem_data()
        sys.stderr.write("BUG: problem data misses required element '{0}'"
                         " required for D-Bus notification\n"
                         .format(str(ex)))

        return_status = RETURN_FAILURE

    if OPTIONS.autoreporting or conf.get("AutoreportingEnabled", "no") == "yes":
        event_name = OPTIONS.autoreporting_event
        if not event_name:
            if "AutoreportingEvent" in conf:
                event_name = conf["AutoreportingEvent"]
            else:
                sys.stderr.write("Autoreporting event is not configured\n")
                return_status = RETURN_FAILURE

        if event_name:
            try:
                run_autoreport(PD, event_name)
            except RuntimeError as ex:
                sys.stderr.write("Cannot notify '{0}' via uReport: {1}\n".
                        format(DIR_PATH, str(ex)))
                return_status = RETURN_FAILURE
            except KeyError as ex:
                # this is a bug in build_notification_problem_data()
                sys.stderr.write(
                    "BUG: problem data misses required element '{0}'"
                    " required for uReport notification\n".format(str(ex)))

                return_status = RETURN_FAILURE

    # log problem to systemd journal with SYSLOG_IDENTIFIER=abrt-notification
    os.environ["REPORTER_JOURNAL_SYSLOG_ID"] = "abrt-notification"
    if not run_event("report_systemd-journal", DIR_PATH):
        # if report_systemd-journal is not defined, run 'reporter-systemd-journald'
        try:
            call(["reporter-systemd-journald", "-d " + DIR_PATH])
        except FileNotFoundError as ex:
            # we expect the reporter doesn't have to be installed
            log1("reporter-systemd-journald was not found")

    sys.exit(return_status)

