/*
  Copyright (C) 2015  ABRT team

  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
*/

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include "abrt_problems2_session.h"

#include "libabrt.h"

static PolkitAuthority *s_pk_authority;

PolkitAuthority *abrt_p2_session_class_set_polkit_authority(PolkitAuthority *pk_authority)
{
    if (s_pk_authority != NULL)
    {
        log_warning("Session: polkit Authority already initialized");

        /*
         * Introduce something like this to libreport
        if (g_verbose > 3)
            abort();
        */
        return s_pk_authority;
    }

    s_pk_authority = pk_authority;

    return s_pk_authority;
}

PolkitAuthority *abrt_p2_session_class_polkit_authority(void)
{
    if (s_pk_authority == NULL)
        log_debug("Session: Polkit Authority not-yet initialized");

    return s_pk_authority;
}

PolkitAuthority *abrt_p2_session_class_release_polkit_authority(void)
{
    PolkitAuthority *pk_authority = s_pk_authority;
    s_pk_authority = NULL;
    return pk_authority;
}

typedef struct
{
    char     *p2s_caller;
    uid_t    p2s_uid;
    int      p2s_state;
    GList    *p2s_tasks;
    uint32_t p2s_task_indexer;
    GHashTable *p2s_tokens;
    struct check_auth_cb_params *p2s_auth_rq;
    PolkitSubject *p2s_pk_subject;
} AbrtP2SessionPrivate;

enum
{
    ABRT_P2_SESSION_STATE_INIT,
    ABRT_P2_SESSION_STATE_PENDING,
    ABRT_P2_SESSION_STATE_AUTH,
};

struct _AbrtP2Session
{
    GObject parent_instance;
    AbrtP2SessionPrivate *pv;
};

G_DEFINE_TYPE_WITH_PRIVATE(AbrtP2Session, abrt_p2_session, G_TYPE_OBJECT)

struct check_auth_cb_params
{
    AbrtP2Session *session;
    GCancellable *cancellable;
    PolkitDetails *details;
};

enum {
    SN_AUTHORIZATION_CHANGED,
    SN_LAST_SIGNAL
};

static guint s_signals[SN_LAST_SIGNAL] = { 0 };

static void abrt_p2_session_finalize(GObject *gobject)
{
    AbrtP2SessionPrivate *pv = abrt_p2_session_get_instance_private(ABRT_P2_SESSION(gobject));
    free(pv->p2s_caller);
    g_hash_table_destroy(pv->p2s_tokens);

    /* If there is ongoing authorization, */
    /* tell the callback that it must not touch session.*/
    if (pv->p2s_auth_rq != NULL)
        pv->p2s_auth_rq->session = NULL;

    if (pv->p2s_pk_subject != NULL)
        g_object_unref(pv->p2s_pk_subject);
}

static void abrt_p2_session_class_init(AbrtP2SessionClass *klass)
{
    GObjectClass *object_class = G_OBJECT_CLASS(klass);
    object_class->finalize = abrt_p2_session_finalize;

    s_signals[SN_AUTHORIZATION_CHANGED] = g_signal_new ("authorization-changed",
                                                        G_TYPE_FROM_CLASS (klass),
                                                        G_SIGNAL_RUN_LAST,
                                                        G_STRUCT_OFFSET(AbrtP2SessionClass, authorization_changed),
                                                        /*accumulator*/NULL, /*accu_data*/NULL,
                                                        g_cclosure_marshal_VOID__INT,
                                                        G_TYPE_NONE,
                                                        /*n_params*/1,
                                                        G_TYPE_INT);
}

static void abrt_p2_session_init(AbrtP2Session *self)
{
    self->pv = abrt_p2_session_get_instance_private(self);
}

static void emit_authorization_changed(AbrtP2Session *session,
            AbrtP2SessionAuthChangedStatus status)
{
    g_signal_emit(session,
                  s_signals[SN_AUTHORIZATION_CHANGED],
                  0,
                  (gint32)status);
}

static void change_state(AbrtP2Session *session, int new_state)
{
    if (session->pv->p2s_state == new_state)
        return;

    AbrtP2SessionAuthChangedStatus value = -1;
    int old_state = session->pv->p2s_state;
    session->pv->p2s_state = new_state;

    if      (old_state == ABRT_P2_SESSION_STATE_INIT    && new_state == ABRT_P2_SESSION_STATE_PENDING)
    {
        log_debug("Authorization request is pending");
        value = ABRT_P2_SESSION_CHANGED_PENDING;
    }
    else if (old_state == ABRT_P2_SESSION_STATE_INIT    && new_state == ABRT_P2_SESSION_STATE_AUTH)
    {
        log_debug("Authorization has been granted");
        value = ABRT_P2_SESSION_CHANGED_AUTHORIZED;
    }
    else if (old_state == ABRT_P2_SESSION_STATE_PENDING && new_state == ABRT_P2_SESSION_STATE_AUTH)
    {
        log_debug("Authorization has been acquired");
        value = ABRT_P2_SESSION_CHANGED_AUTHORIZED;
    }
    else if (old_state == ABRT_P2_SESSION_STATE_AUTH    && new_state == ABRT_P2_SESSION_STATE_INIT)
    {
        log_debug("Authorization request has been lost");
        value = ABRT_P2_SESSION_CHANGED_NOT_AUTHORIZED;
    }
    else if (old_state == ABRT_P2_SESSION_STATE_PENDING && new_state == ABRT_P2_SESSION_STATE_INIT)
    {
        log_debug("Authorization request has failed");
        value = ABRT_P2_SESSION_CHANGED_FAILED;
    }
    else
        goto forgotten_state;

    emit_authorization_changed(session, value);
    return;

forgotten_state:
    error_msg("BUG: unsupported state, current : %d, new : %d",
              session->pv->p2s_state,
              new_state);
}

#ifdef HAVE_POLKIT
static void check_authorization_callback(GObject *source,
            GAsyncResult *res,
            gpointer user_data)
{
    GError *error = NULL;
    PolkitAuthorizationResult *result = NULL;
    result = polkit_authority_check_authorization_finish(POLKIT_AUTHORITY(source),
                                                         res,
                                                         &error);

    int new_state = ABRT_P2_SESSION_STATE_INIT;
    if (result == NULL)
    {
       error_msg("Polkit authorization failed: %s", error->message);
       g_error_free(error);
    }
    else
    {
        if (polkit_authorization_result_get_is_authorized(result))
            new_state = ABRT_P2_SESSION_STATE_AUTH;
        else
            /* We do not support polkit_authorization_result_get_is_challenge */
            log_debug("Not authorized");

        g_object_unref(result);
    }

    struct check_auth_cb_params *params = (struct check_auth_cb_params *)user_data;
    AbrtP2Session *session = params->session;
    g_object_unref(params->cancellable);

    if (params->details != NULL)
        g_object_unref(params->details);

    free(params);

    /* It might happen that the session had been destroyed before the callback
     * was called (e.g. if you programmatically cancel the operation, this
     * callback is called in the next main loop iteration). */
    if (session != NULL)
    {
        change_state(session, new_state);
        session->pv->p2s_auth_rq = NULL;
    }
    else
        log_debug("Operation finished after the session had been destroyed");
}
#endif

static void authorization_request_initialize(AbrtP2Session *session, GVariant *parameters)
{
#ifdef HAVE_POLKIT
    struct check_auth_cb_params *auth_rq = xmalloc(sizeof(*auth_rq));
    auth_rq->session = session;
    auth_rq->cancellable = g_cancellable_new();

    auth_rq->details = NULL;
    if (parameters != NULL)
    {
        GVariant *message = g_variant_lookup_value(parameters,
                                                   "message",
                                                   G_VARIANT_TYPE_STRING);

        if (message != NULL)
        {
            auth_rq->details = polkit_details_new();
            polkit_details_insert(auth_rq->details,
                                  "polkit.message",
                                  g_variant_get_string(message,
                                                       NULL));

            g_variant_unref(message);
        }
    }

    session->pv->p2s_auth_rq = auth_rq;
    change_state(session, ABRT_P2_SESSION_STATE_PENDING);

    /* http://www.freedesktop.org/software/polkit/docs/latest/polkit-apps.html
     */
    if (session->pv->p2s_pk_subject == NULL)
        session->pv->p2s_pk_subject = polkit_system_bus_name_new(session->pv->p2s_caller);

    /* If you cancel this operation programmatically, you might get the
     * following warning message:
         WARNING **: Error cancelling authorization check:
         GDBus.Error:org.freedesktop.PolicyKit1.Error.Failed:
         No such cancellation_id `cancellation-id-1' for name :1.257

       The message is returned by polkitbackend and it just means that there
       were nothing to cancel. Probably because the operation was already
       finished or not-yet started.
       */
    polkit_authority_check_authorization(abrt_p2_session_class_polkit_authority(),
                                         session->pv->p2s_pk_subject,
                                         "org.freedesktop.problems.getall",
                                         auth_rq->details,
                                         POLKIT_CHECK_AUTHORIZATION_FLAGS_ALLOW_USER_INTERACTION,
                                         auth_rq->cancellable,
                                         check_authorization_callback,
                                         auth_rq);
#else
    change_state(session, ABRT_P2_SESSION_STATE_AUTH);
#endif
}

AbrtP2Session *abrt_p2_session_new(char *caller, uid_t uid)
{
    AbrtP2Session *session = g_object_new(TYPE_ABRT_P2_SESSION, NULL);
    session->pv->p2s_caller = caller;
    session->pv->p2s_uid = uid;

    if (session->pv->p2s_uid == 0)
        session->pv->p2s_state = ABRT_P2_SESSION_STATE_AUTH;
    else
        session->pv->p2s_state = ABRT_P2_SESSION_STATE_INIT;

    session->pv->p2s_tokens = g_hash_table_new_full(g_str_hash,
                                                    g_str_equal,
                                                    g_free,
                                                    NULL);

    return session;
}

const char *abrt_p2_session_generate_token(AbrtP2Session *session,
            unsigned int duration,
            GError **error)
{
    if (session->pv->p2s_state != ABRT_P2_SESSION_STATE_AUTH)
    {
        g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
                    "Session is not authorized");
        return NULL;
    }

#define SESSION_TOKEN_LENGTH 16
    static const char *const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

    if (duration == 0)
        duration = 5;

    FILE *urandom = fopen("/dev/urandom", "rb");

    if (urandom == NULL)
    {
        perror_msg("fopen(/dev/urandom, rb)");
        g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
                    "Failed to open /dev/urandom for reading");
        return NULL;
    }

    unsigned int seed = 0;
    const size_t r = fread(&seed, 1, sizeof(seed), urandom);
    fclose(urandom);

    if (sizeof(seed) != r)
    {
        perror_msg("fread(unsigned int, /dev/urandom)");
        g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
                    "Failed to read 'unsigned int' from /dev/urandom");
        return NULL;
    }

    char *token = xmalloc((SESSION_TOKEN_LENGTH + 1) * sizeof(char));
    for (char *iter = token; iter < token + SESSION_TOKEN_LENGTH; ++iter)
        *iter = alphabet[(int)(strlen(alphabet) * (rand_r(&seed) / (double)RAND_MAX))];

    token[SESSION_TOKEN_LENGTH] = '\0';

    const time_t curtime = time(NULL);
    if (curtime == ((time_t) -1))
    {
        g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
                    "Cannot get current time");
        free(token);
        return NULL;
    }

    g_hash_table_insert(session->pv->p2s_tokens,
                        token,
                        GINT_TO_POINTER(curtime + duration));

    return token;
#undef SESSION_TOKEN_LENGTH
}

int abrt_p2_session_revoke_token(AbrtP2Session *session,
            const char *token)
{
    return g_hash_table_remove(session->pv->p2s_tokens, token) ? 0 : 1;
}

static AbrtP2SessionAuthRequestRet abrt_p2_session_authorize_peer_with_token(
            AbrtP2Session *session,
            AbrtP2Session *peer_session,
            const char *token,
            GError **error)
{
    if (session->pv->p2s_state != ABRT_P2_SESSION_STATE_AUTH)
    {
        g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_ACCESS_DENIED,
                    "Not authorized session cannot pass authorization");
        return ABRT_P2_SESSION_AUTHORIZE_FAILED;
    }

    if (session->pv->p2s_uid != peer_session->pv->p2s_uid)
    {
        g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_ACCESS_DENIED,
                    "Session owners do not match");
        return ABRT_P2_SESSION_AUTHORIZE_FAILED;
    }

    const gpointer expire = g_hash_table_lookup(session->pv->p2s_tokens, token);
    if (expire == NULL)
    {
        g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_ACCESS_DENIED,
                    "No such token");
        return ABRT_P2_SESSION_AUTHORIZE_FAILED;
    }

    const time_t curtime = time(NULL);
    if (curtime == ((time_t) -1))
    {
        g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
                    "Cannot get current time");
        return ABRT_P2_SESSION_AUTHORIZE_FAILED;
    }

    if (curtime > (time_t)(GPOINTER_TO_INT(expire)))
    {
        g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_ACCESS_DENIED,
                    "Token has already expired");
        return ABRT_P2_SESSION_AUTHORIZE_FAILED;
    }

    g_hash_table_remove(session->pv->p2s_tokens, token);
    abrt_p2_session_grant_authorization(peer_session);

    log_info("Granted authorization to peer session on '%s' bus",
             peer_session->pv->p2s_caller);

    return ABRT_P2_SESSION_AUTHORIZE_GRANTED;
}

static int abrt_p2_session_cmp_caller(AbrtP2Session *lhs, const char *bus_name)
{
    return strcmp(lhs->pv->p2s_caller, bus_name);
}

static AbrtP2SessionAuthRequestRet _abrt_p2_session_begin_auth(AbrtP2Session *session,
            GVariant *parameters,
            GList *peers,
            GError **error)
{
    AbrtP2SessionAuthRequestRet ret = ABRT_P2_SESSION_AUTHORIZE_FAILED;

    GVariant *peer_bus = g_variant_lookup_value(parameters,
                                                "problems2.peer-bus",
                                                G_VARIANT_TYPE_STRING);

    GVariant *peer_token = g_variant_lookup_value(parameters,
                                             "problems2.peer-token",
                                             G_VARIANT_TYPE_STRING);

    if (!peer_bus && !peer_token)
    {
        authorization_request_initialize(session, parameters);
        ret = ABRT_P2_SESSION_AUTHORIZE_ACCEPTED;
    }
    else if (peer_bus && peer_token)
    {
        const gchar *bus = g_variant_get_string(peer_bus, NULL);
        GList *tmp = g_list_find_custom(peers,
                                        bus,
                                        (GCompareFunc)abrt_p2_session_cmp_caller);
        if (tmp == NULL)
        {
            g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
                        "No peer session for bus '%s'",
                        g_variant_get_string(peer_bus, NULL));
            goto finito;
        }

        AbrtP2Session *peer_session = (AbrtP2Session *)tmp->data;
        const gchar *token = g_variant_get_string(peer_token, NULL);
        ret = abrt_p2_session_authorize_peer_with_token(peer_session,
                                                        session,
                                                        token,
                                                        error);
    }
    else /* if ((peer_bus && !peer_token) || (!peer_bus && peer_token)) */
    {
        g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
                    "Invalid parameters peer-bus and peer-token.");
    }

finito:
    if (peer_bus != NULL)
        g_variant_unref(peer_bus);

    if (peer_token != NULL)
        g_variant_unref(peer_token);

    return ret;
}

AbrtP2SessionAuthRequestRet abrt_p2_session_authorize(AbrtP2Session *session,
            GVariant *parameters,
            GList *peers,
            GError **error)
{
    switch(session->pv->p2s_state)
    {
        case ABRT_P2_SESSION_STATE_INIT:
            return _abrt_p2_session_begin_auth(session, parameters, peers, error);

        case ABRT_P2_SESSION_STATE_PENDING:
            return ABRT_P2_SESSION_AUTHORIZE_PENDING;

        case ABRT_P2_SESSION_STATE_AUTH:
            return ABRT_P2_SESSION_AUTHORIZE_GRANTED;

        default:
            error_msg("BUG: %s: forgotten state -> %d", __func__, session->pv->p2s_state);
            return ABRT_P2_SESSION_AUTHORIZE_FAILED;
    }

}

void abrt_p2_session_revoke_authorization(AbrtP2Session *session)
{
    if (session->pv->p2s_uid == 0)
        return;

    switch(session->pv->p2s_state)
    {
        case ABRT_P2_SESSION_STATE_AUTH:
            change_state(session, ABRT_P2_SESSION_STATE_INIT);
            break;

        case ABRT_P2_SESSION_STATE_PENDING:
            g_cancellable_cancel(session->pv->p2s_auth_rq->cancellable);
            change_state(session, ABRT_P2_SESSION_STATE_INIT);
            break;

        case ABRT_P2_SESSION_STATE_INIT:
            /* pass */
            break;
    }
}

AbrtP2SessionAuthRequestRet abrt_p2_session_grant_authorization(AbrtP2Session *session)
{
    switch(session->pv->p2s_state)
    {
        case ABRT_P2_SESSION_STATE_AUTH:
            /* pass */
            break;

        case ABRT_P2_SESSION_STATE_PENDING:
            g_cancellable_cancel(session->pv->p2s_auth_rq->cancellable);
            change_state(session, ABRT_P2_SESSION_STATE_AUTH);
            break;

        case ABRT_P2_SESSION_STATE_INIT:
            change_state(session, ABRT_P2_SESSION_STATE_AUTH);
            break;
    }

    return ABRT_P2_SESSION_AUTHORIZE_GRANTED;
}

uid_t abrt_p2_session_uid(AbrtP2Session *session)
{
    return session->pv->p2s_uid;
}

const char *abrt_p2_session_caller(AbrtP2Session *session)
{
    return session->pv->p2s_caller;
}

int abrt_p2_session_is_authorized(AbrtP2Session *session)
{
    return session->pv->p2s_state == ABRT_P2_SESSION_STATE_AUTH;
}

int abrt_p2_session_check_sanity(AbrtP2Session *session,
            const char *caller,
            uid_t caller_uid,
            GError **error)
{
    if (strcmp(session->pv->p2s_caller, caller) == 0 && session->pv->p2s_uid == caller_uid)
        /* the session node is sane */
        return 0;

    log_warning("Problems2 Session object does not belong to UID %d", caller_uid);

    g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
                "Your Problems2 Session is broken. Check system logs for more details.");
    return -1;
}

uint32_t abrt_p2_session_add_task(AbrtP2Session *session,
            AbrtP2Task *task,
            GError **error)
{
    if (session->pv->p2s_task_indexer == (UINT32_MAX - 1))
    {
        g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
                    "Reached the limit of task per session.");

        return UINT32_MAX;
    }

    if (abrt_p2_session_owns_task(session, task) == 0)
    {
        g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
                    "Task is already owned by the session");

        return UINT32_MAX;
    }

    session->pv->p2s_tasks = g_list_prepend(session->pv->p2s_tasks, task);

    return session->pv->p2s_task_indexer++;
}

void abrt_p2_session_remove_task(AbrtP2Session *session,
            AbrtP2Task *task,
            GError **error)
{
    session->pv->p2s_tasks = g_list_remove(session->pv->p2s_tasks, task);
}

GList *abrt_p2_session_tasks(AbrtP2Session *session)
{
    return session->pv->p2s_tasks;
}

int abrt_p2_session_owns_task(AbrtP2Session *session,
            AbrtP2Task *task)
{
    return !(g_list_find(session->pv->p2s_tasks, task));
}

int abrt_p2_session_tasks_count(AbrtP2Session *session)
{
    return g_list_length(session->pv->p2s_tasks);
}

static void abrt_p2_session_dispose_task(AbrtP2Task *task,
            gint32 status)
{
    switch(status)
    {
        case ABRT_P2_TASK_STATUS_STOPPED:
            {
                GError *local_error = NULL;
                abrt_p2_task_cancel(task, &local_error);
                if (local_error != NULL)
                {
                    error_msg("Task garbage collector failed to cancel task: %s",
                              local_error->message);

                    g_error_free(local_error);
                }

                /* In case of errors, this could cause problems, but I
                 * don't have better plan yet. */
                log_debug("Disposed new/stopped task: %p", task);
                g_object_unref(task);
            }
            break;

        case ABRT_P2_TASK_STATUS_NEW:
            log_debug("Disposed new task: %p", task);
            g_object_unref(task);
            break;

        case ABRT_P2_TASK_STATUS_FAILED:
            log_debug("Disposed failed task: %p", task);
            g_object_unref(task);
            break;

        case ABRT_P2_TASK_STATUS_CANCELED:
            log_debug("Disposed canceled task: %p", task);
            g_object_unref(task);
            break;

        case ABRT_P2_TASK_STATUS_DONE:
            log_debug("Disposed done task: %p", task);
            g_object_unref(task);
            break;

        case ABRT_P2_TASK_STATUS_RUNNING:
            error_msg("BUG: cannot dispose RUNNING task");
            abort();
            break;
    }
}

static void abrt_p2_session_delayed_dispose_task(AbrtP2Task *task,
            gint32 status,
            gpointer user_data)
{
    if (status == ABRT_P2_TASK_STATUS_RUNNING)
    {
        error_msg("BUG: task to dispose must not change state to RUNNING");

        abort();
    }

    log_debug("Going to dispose delayed task: %p: %d",
              task,
              status);

    abrt_p2_session_dispose_task(task, status);
}

void abrt_p2_session_clean_tasks(AbrtP2Session *session)
{
    GList *task = session->pv->p2s_tasks;
    session->pv->p2s_tasks = NULL;

    while (task != NULL)
    {
        AbrtP2Task *t = ABRT_P2_TASK(task->data);

        task = g_list_delete_link(task, task);

        const AbrtP2TaskStatus status = abrt_p2_task_status(t);
        if (status != ABRT_P2_TASK_STATUS_RUNNING)
        {
            abrt_p2_session_dispose_task(t, status);
            continue;
        }

        log_debug("Delaying disposal of running task: %p", t);

        g_signal_connect(t,
                         "status-changed",
                         G_CALLBACK(abrt_p2_session_delayed_dispose_task),
                         NULL);

        GError *local_error = NULL;
        abrt_p2_task_cancel(t, &local_error);

        if (local_error != NULL)
        {
            error_msg("Task garbage collector failed to cancel running task: %s",
                      local_error->message);

            g_error_free(local_error);
        }
    }
}
