#include "Fingerprint.hpp"
#include "../core/hyprlock.hpp"
#include "../core/Dbus.hpp"
#include "../helpers/Log.hpp"
#include "../config/ConfigManager.hpp"

#include <memory>
#include <unistd.h>
#include <pwd.h>

#include <cstring>

static const auto FPRINT        = sdbus::ServiceName{"net.reactivated.Fprint"};
static const auto DEVICE        = sdbus::ServiceName{"net.reactivated.Fprint.Device"};
static const auto MANAGER       = sdbus::ServiceName{"net.reactivated.Fprint.Manager"};
static const auto LOGIN_MANAGER = sdbus::ServiceName{"org.freedesktop.login1.Manager"};

enum MatchResult {
    MATCH_INVALID = 0,
    MATCH_NO_MATCH,
    MATCH_MATCHED,
    MATCH_RETRY,
    MATCH_SWIPE_TOO_SHORT,
    MATCH_FINGER_NOT_CENTERED,
    MATCH_REMOVE_AND_RETRY,
    MATCH_DISCONNECTED,
    MATCH_UNKNOWN_ERROR,
};

static std::map<std::string, MatchResult> s_mapStringToTestType = {{"verify-no-match", MATCH_NO_MATCH},
                                                                   {"verify-match", MATCH_MATCHED},
                                                                   {"verify-retry-scan", MATCH_RETRY},
                                                                   {"verify-swipe-too-short", MATCH_SWIPE_TOO_SHORT},
                                                                   {"verify-finger-not-centered", MATCH_FINGER_NOT_CENTERED},
                                                                   {"verify-remove-and-retry", MATCH_REMOVE_AND_RETRY},
                                                                   {"verify-disconnected", MATCH_DISCONNECTED},
                                                                   {"verify-unknown-error", MATCH_UNKNOWN_ERROR}};

CFingerprint::CFingerprint() {
    static const auto FINGERPRINTREADY   = g_pConfigManager->getValue<Hyprlang::STRING>("auth:fingerprint:ready_message");
    m_sFingerprintReady                  = *FINGERPRINTREADY;
    static const auto FINGERPRINTPRESENT = g_pConfigManager->getValue<Hyprlang::STRING>("auth:fingerprint:present_message");
    m_sFingerprintPresent                = *FINGERPRINTPRESENT;
}

CFingerprint::~CFingerprint() {
    ;
}

void CFingerprint::init() {
    if (!g_dbus->m_connection || !g_dbus->m_freedesktopLogin1) {
        Log::logger->log(Log::ERR, "fprint: Missing dbus proxy?");
        return;
    }

    g_dbus->m_freedesktopLogin1->getPropertyAsync("PreparingForSleep")
        .onInterface(LOGIN_MANAGER)
        .uponReplyInvoke([this](std::optional<sdbus::Error> e, sdbus::Variant preparingForSleep) {
            if (e) {
                Log::logger->log(Log::WARN, "fprint: Failed getting value for PreparingForSleep: {}", e->what());
                return;
            }
            m_sDBUSState.sleeping = preparingForSleep.get<bool>();
            // When entering sleep, the wake signal will trigger startVerify().
            if (m_sDBUSState.sleeping)
                return;

            startVerify();
        });

    g_dbus->m_freedesktopLogin1->uponSignal("PrepareForSleep").onInterface(LOGIN_MANAGER).call([this](bool start) {
        Log::logger->log(Log::INFO, "fprint: PrepareForSleep (start: {})", start);
        m_sDBUSState.sleeping = start;
        if (!m_sDBUSState.sleeping && !m_sDBUSState.verifying)
            startVerify();
    });
}

void CFingerprint::handleInput(const std::string& input) {
    ;
}

std::optional<std::string> CFingerprint::getLastFailText() {
    if (!m_sFailureReason.empty())
        return std::optional(m_sFailureReason);
    return std::nullopt;
}

std::optional<std::string> CFingerprint::getLastPrompt() {
    if (!m_sPrompt.empty())
        return std::optional(m_sPrompt);
    return std::nullopt;
}

bool CFingerprint::checkWaiting() {
    return false;
}

void CFingerprint::terminate() {
    if (!m_sDBUSState.abort)
        releaseDevice();
}

bool CFingerprint::createDeviceProxy() {
    auto              proxy = sdbus::createProxy(*g_dbus->m_connection, FPRINT, sdbus::ObjectPath{"/net/reactivated/Fprint/Manager"});

    sdbus::ObjectPath path;
    try {
        proxy->callMethod("GetDefaultDevice").onInterface(MANAGER).storeResultsTo(path);
    } catch (sdbus::Error& e) {
        Log::logger->log(Log::WARN, "fprint: couldn't connect to Fprint service ({})", e.what());
        return false;
    }
    Log::logger->log(Log::INFO, "fprint: using device path {}", path.c_str());
    m_sDBUSState.device = sdbus::createProxy(*g_dbus->m_connection, FPRINT, path);

    m_sDBUSState.device->uponSignal("VerifyFingerSelected").onInterface(DEVICE).call([](const std::string& finger) {
        Log::logger->log(Log::INFO, "fprint: finger selected: {}", finger);
    });
    m_sDBUSState.device->uponSignal("VerifyStatus").onInterface(DEVICE).call([this](const std::string& result, const bool done) { handleVerifyStatus(result, done); });

    m_sDBUSState.device->uponSignal("PropertiesChanged")
        .onInterface("org.freedesktop.DBus.Properties")
        .call([this](const std::string& interface, const std::map<std::string, sdbus::Variant>& properties) {
            if (interface != DEVICE || m_sDBUSState.done)
                return;

            try {
                const auto presentVariant = properties.at("finger-present");
                bool       isPresent      = presentVariant.get<bool>();
                if (!isPresent)
                    return;
                m_sPrompt = m_sFingerprintPresent;
                g_pHyprlock->enqueueForceUpdateTimers();
            } catch (std::out_of_range& e) {}
        });

    return true;
}

void CFingerprint::handleVerifyStatus(const std::string& result, bool done) {
    Log::logger->log(Log::INFO, "fprint: handling status {}", result);
    auto matchResult   = s_mapStringToTestType[result];
    bool authenticated = false;
    bool retry         = false;
    if (m_sDBUSState.sleeping) {
        stopVerify();
        Log::logger->log(Log::INFO, "fprint: device suspended");
        return;
    }
    switch (matchResult) {
        case MATCH_INVALID: Log::logger->log(Log::WARN, "fprint: unknown status: {}", result); break;
        case MATCH_NO_MATCH:
            stopVerify();
            if (m_sDBUSState.retries >= 3) {
                m_sFailureReason = "Fingerprint auth disabled (too many failed attempts)";
            } else {
                done                         = false;
                static const auto RETRYDELAY = g_pConfigManager->getValue<Hyprlang::INT>("auth:fingerprint:retry_delay");
                g_pHyprlock->addTimer(std::chrono::milliseconds(*RETRYDELAY), [](ASP<CTimer> self, void* data) { ((CFingerprint*)data)->startVerify(true); }, this);
                m_sFailureReason = "Fingerprint did not match";
            }
            break;
        case MATCH_UNKNOWN_ERROR:
            stopVerify();
            m_sFailureReason = "Fingerprint auth disabled (unknown error)";
            break;
        case MATCH_MATCHED:
            stopVerify();
            authenticated = true;
            g_pAuth->enqueueUnlock();
            break;
        case MATCH_RETRY:
            retry     = true;
            m_sPrompt = "Please retry fingerprint scan";
            break;
        case MATCH_SWIPE_TOO_SHORT:
            retry     = true;
            m_sPrompt = "Swipe too short - try again";
            break;
        case MATCH_FINGER_NOT_CENTERED:
            retry     = true;
            m_sPrompt = "Finger not centered - try again";
            break;
        case MATCH_REMOVE_AND_RETRY:
            retry     = true;
            m_sPrompt = "Remove your finger and try again";
            break;
        case MATCH_DISCONNECTED:
            m_sFailureReason   = "Fingerprint device disconnected";
            m_sDBUSState.abort = true;
            break;
    }

    if (!authenticated && !retry)
        g_pAuth->enqueueFail(m_sFailureReason, AUTH_IMPL_FINGERPRINT);
    else if (retry)
        g_pHyprlock->enqueueForceUpdateTimers();

    if (done || m_sDBUSState.abort)
        m_sDBUSState.done = true;
}

void CFingerprint::claimDevice() {
    const auto currentUser = ""; // Empty string means use the caller's id.
    m_sDBUSState.device->callMethodAsync("Claim").onInterface(DEVICE).withArguments(currentUser).uponReplyInvoke([this](std::optional<sdbus::Error> e) {
        if (e)
            Log::logger->log(Log::WARN, "fprint: could not claim device, {}", e->what());
        else {
            Log::logger->log(Log::INFO, "fprint: claimed device");
            startVerify();
        }
    });
}

void CFingerprint::startVerify(bool isRetry) {
    m_sDBUSState.verifying = true;
    if (!m_sDBUSState.device) {
        if (!createDeviceProxy())
            return;

        claimDevice();
        return;
    }
    auto finger = "any"; // Any finger.
    m_sDBUSState.device->callMethodAsync("VerifyStart").onInterface(DEVICE).withArguments(finger).uponReplyInvoke([this, isRetry](std::optional<sdbus::Error> e) {
        if (e) {
            Log::logger->log(Log::WARN, "fprint: could not start verifying, {}", e->what());
            if (isRetry)
                m_sFailureReason = "Fingerprint auth disabled (failed to restart)";

        } else {
            Log::logger->log(Log::INFO, "fprint: started verifying");
            if (isRetry) {
                m_sDBUSState.retries++;
                m_sPrompt = "Could not match fingerprint. Try again.";
            } else
                m_sPrompt = m_sFingerprintReady;
        }
        g_pHyprlock->enqueueForceUpdateTimers();
    });
}

bool CFingerprint::stopVerify() {
    m_sDBUSState.verifying = false;
    if (!m_sDBUSState.device)
        return false;
    try {
        m_sDBUSState.device->callMethod("VerifyStop").onInterface(DEVICE);
    } catch (sdbus::Error& e) {
        Log::logger->log(Log::WARN, "fprint: could not stop verifying, {}", e.what());
        return false;
    }
    Log::logger->log(Log::INFO, "fprint: stopped verification");
    return true;
}

bool CFingerprint::releaseDevice() {
    if (!m_sDBUSState.device)
        return false;
    try {
        m_sDBUSState.device->callMethod("Release").onInterface(DEVICE);
    } catch (sdbus::Error& e) {
        Log::logger->log(Log::WARN, "fprint: could not release device, {}", e.what());
        m_sDBUSState.device.reset();
        return false;
    }

    m_sDBUSState.device.reset();
    Log::logger->log(Log::INFO, "fprint: released device");
    return true;
}
