/*
 * Copyright (C) 2014 Canonical, Ltd.
 *
 * 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; version 3.
 *
 * 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, see <http://www.gnu.org/licenses/>.
 */

#include "polkitlistener.h"

#include <condition_variable>
#include <iostream>
#include <string>
#include <thread>
#include <unistd.h>

#include <gio/gio.h>

#define UNKNOWN        -1
#define SUCCESS         0
#define CANNOT_RUN      1
#define CANNOT_REGISTER 2
#define CANNOT_INIT     3
#define BAD_PPID        4
#define FAILURE         5

typedef enum {
    ACT_USER_PASSWORD_MODE_REGULAR,
    ACT_USER_PASSWORD_MODE_SET_AT_LOGIN,
    ACT_USER_PASSWORD_MODE_NONE,
} ActUserPasswordMode;

static int success_code = UNKNOWN;

// Wait for the polkit agent to be up
static std::condition_variable agent_spawned_condition;
static std::mutex agent_spawned_mutex;
static bool agent_spawned = false;

void agent_spawned_func() {
    std::lock_guard<std::mutex> lock(agent_spawned_mutex);
    agent_spawned = true;
    agent_spawned_condition.notify_all();
}

static void polkitRunner(std::string password) {
    UssPolkitListener *polkitListener = uss_polkit_listener_new();
    if (polkitListener == nullptr) {
        std::lock_guard<std::mutex> lock(agent_spawned_mutex);
        success_code = CANNOT_INIT;
        agent_spawned_condition.notify_all();
        return;
    }

    // Get pid
    pid_t ppid = getppid();
    if (ppid == 1) {
        uss_polkit_listener_free(polkitListener);

        std::lock_guard<std::mutex> lock(agent_spawned_mutex);
        success_code = BAD_PPID;
        agent_spawned_condition.notify_all();
        return;
    }
    uss_polkit_listener_set_pid(polkitListener, ppid);

    // Set password
    uss_polkit_listener_set_password(polkitListener, password.c_str());

    // Register agent
    if (!uss_polkit_listener_register(polkitListener)) {
        uss_polkit_listener_free(polkitListener);

        std::lock_guard<std::mutex> lock(agent_spawned_mutex);
        success_code = CANNOT_REGISTER;
        agent_spawned_condition.notify_all();
        return;
    }

    g_idle_add_once(reinterpret_cast<GSourceOnceFunc>(&agent_spawned_func), nullptr);

    if (!uss_polkit_listener_run(polkitListener)) {
        uss_polkit_listener_free(polkitListener);

        std::lock_guard<std::mutex> lock(agent_spawned_mutex);
        success_code = CANNOT_RUN;
        agent_spawned_condition.notify_all();
        return;
    }

    uss_polkit_listener_free(polkitListener);
}

int main()
{
    // Read parameters
    std::string password;
    std::string mode;
    std::getline(std::cin, password);
    std::getline(std::cin, mode);

    std::thread polkitThread(polkitRunner, password);

    {
        std::unique_lock<std::mutex> lock(agent_spawned_mutex);
        agent_spawned_condition.wait(lock, []{
            return agent_spawned || success_code != UNKNOWN;
        });
    }

    if (success_code != UNKNOWN) {
        return success_code;
    }

    const ActUserPasswordMode newMode = static_cast<ActUserPasswordMode>(std::stoi(mode));

    const auto path = "/org/freedesktop/Accounts/User" + std::to_string(geteuid());
    GDBusProxy *proxy = g_dbus_proxy_new_for_bus_sync(G_BUS_TYPE_SYSTEM,
                                                      G_DBUS_PROXY_FLAGS_NONE,
                                                      nullptr,
                                                      "org.freedesktop.Accounts",
                                                      path.c_str(),
                                                      "org.freedesktop.Accounts.User",
                                                      nullptr,
                                                      nullptr);
    if (!proxy) {
        success_code = FAILURE;
        return success_code;
    }

    /* We call SetPasswordMode directly over DBus ourselves, rather than rely
       on the act_user_set_password_mode call, because that call gives no
       feedback! How hard would it have been to add a bool return? Ah well. */
    GError* error = nullptr;
    g_dbus_proxy_call_sync(proxy,
                           "SetPasswordMode",
                           g_variant_new ("(i)", newMode),
                           G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION,
                           -1,
                           NULL,
                           &error);
    if (error) {
        const auto dbus_error = std::string(g_dbus_error_get_remote_error(error));

        // We allow "org.freedesktop.Accounts.Error.Failed" because we actually
        // expect that error in some cases.  In Ubuntu Touch, group memberships
        // are not allowed to be changed (/etc/group is read-only).  So when
        // AccountsService tries to add/remove the user from the nopasswdlogin
        // group, it will fail.  Thankfully, this will be after it does what we
        // actually care about it doing (deleting user password).  But it will
        // return an error in this case, with a message about gpasswd failing
        // and the above error name.  In other cases (like bad authentication),
        // it will return something else (like Error.PermissionDenied).
        if (dbus_error == "org.freedesktop.Accounts.Error.Failed") {
            success_code = SUCCESS;
        } else {
            std::cerr << "Could not set password mode: " << dbus_error << std::endl;
            success_code = FAILURE;
        }
    } else {
        success_code = SUCCESS;
    }

    polkitThread.join();
    return success_code;

}
