#include "authservice.h"

#include <QCryptographicHash>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QJsonDocument>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QUuid>

namespace {

const QString kAuthServer = QStringLiteral("https://authserver.ely.by");
const QString kInjectorManifest = QStringLiteral("https://authlib-injector.yushi.moe/artifact/latest.json");
const QString kInjectorFileName = QStringLiteral("authlib-injector.jar");

// Ely.by отдаёт UUID без дефисов, вручную введённый мог прийти и с ними.
QString normalizeUuid(const QString &uuid)
{
    return QString(uuid).remove(u'-');
}

QString describeElyError(const QJsonObject &response, int status)
{
    const QString message = response.value(QStringLiteral("errorMessage")).toString();
    if (!message.isEmpty())
        return message;
    const QString error = response.value(QStringLiteral("error")).toString();
    if (!error.isEmpty())
        return error;
    return QObject::tr("Сервер авторизации вернул код %1").arg(status);
}

} // namespace

AuthService::AuthService(QObject *parent)
    : QObject(parent)
    , m_network(new QNetworkAccessManager(this))
{
}

AuthResult AuthService::offline(const QString &nickname)
{
    AuthResult result;
    if (nickname.isEmpty()) {
        result.error = tr("Для офлайн-запуска нужен никнейм");
        return result;
    }

    QByteArray digest = QCryptographicHash::hash(
        QStringLiteral("OfflinePlayer:%1").arg(nickname).toUtf8(), QCryptographicHash::Md5);
    digest[6] = static_cast<char>((digest[6] & 0x0f) | 0x30);   // версия UUID = 3
    digest[8] = static_cast<char>((digest[8] & 0x3f) | 0x80);   // вариант RFC 4122

    result.ok = true;
    result.playerName = nickname;
    result.uuid = QString::fromLatin1(digest.toHex());
    result.accessToken = QStringLiteral("0");
    result.clientToken = QStringLiteral("0");
    result.userType = QStringLiteral("legacy");
    return result;
}

QString AuthService::generateClientToken()
{
    return QUuid::createUuid().toString(QUuid::WithoutBraces);
}

void AuthService::postJson(
    const QString &endpoint,
    const QJsonObject &body,
    std::function<void(int, const QJsonObject &, const QString &)> handler)
{
    QNetworkRequest request{QUrl(kAuthServer + endpoint)};
    request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
    request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
                         QNetworkRequest::NoLessSafeRedirectPolicy);

    QNetworkReply *reply = m_network->post(request, QJsonDocument(body).toJson(QJsonDocument::Compact));
    connect(reply, &QNetworkReply::finished, this, [reply, handler = std::move(handler)]() {
        reply->deleteLater();

        const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
        const QByteArray payload = reply->readAll();

        // Сетевую ошибку отличаем от ответа сервера: при 401 статус есть, а error — есть всегда.
        QString transportError;
        if (status == 0 && reply->error() != QNetworkReply::NoError)
            transportError = reply->errorString();

        handler(status, QJsonDocument::fromJson(payload).object(), transportError);
    });
}

void AuthService::loginElyBy(const QString &login,
                             const QString &password,
                             const QString &clientToken,
                             const QString &accessToken,
                             Callback callback)
{
    if (login.isEmpty()) {
        AuthResult result;
        result.error = tr("Укажите логин Ely.by в настройках профиля");
        callback(result);
        return;
    }

    const QString token = clientToken.isEmpty() ? generateClientToken() : clientToken;

    // Токена ещё нет — сразу за паролем.
    if (accessToken.isEmpty()) {
        authenticate(login, password, token, std::move(callback));
        return;
    }

    emit progress(tr("Проверка сохранённого токена Ely.by…"));
    postJson(QStringLiteral("/auth/validate"),
             {{QStringLiteral("accessToken"), accessToken}},
             [this, login, password, token, accessToken, callback](
                 int status, const QJsonObject &, const QString &transportError) {
                 if (!transportError.isEmpty()) {
                     AuthResult result;
                     result.error = tr("Нет связи с authserver.ely.by: %1").arg(transportError);
                     callback(result);
                     return;
                 }

                 if (status == 200) {
                     AuthResult result;
                     result.ok = true;
                     result.accessToken = accessToken;
                     result.clientToken = token;
                     result.userType = QStringLiteral("ELYBY");
                     callback(result);
                     return;
                 }

                 // Токен протух — пробуем продлить его, не спрашивая пароль.
                 emit progress(tr("Обновление токена Ely.by…"));
                 postJson(QStringLiteral("/auth/refresh"),
                          {{QStringLiteral("accessToken"), accessToken},
                           {QStringLiteral("clientToken"), token},
                           {QStringLiteral("requestUser"), true}},
                          [this, login, password, token, callback](
                              int refreshStatus, const QJsonObject &response, const QString &error) {
                              if (error.isEmpty() && refreshStatus == 200) {
                                  AuthResult result;
                                  result.ok = true;
                                  result.accessToken = response.value(QStringLiteral("accessToken")).toString();
                                  result.clientToken = token;
                                  const QJsonObject profile =
                                      response.value(QStringLiteral("selectedProfile")).toObject();
                                  result.playerName = profile.value(QStringLiteral("name")).toString();
                                  result.uuid = normalizeUuid(profile.value(QStringLiteral("id")).toString());
                                  result.userType = QStringLiteral("ELYBY");
                                  callback(result);
                                  return;
                              }
                              authenticate(login, password, token, callback);
                          });
             });
}

void AuthService::loginElyByWithTotp(const QString &login,
                                     const QString &password,
                                     const QString &totp,
                                     const QString &clientToken,
                                     Callback callback)
{
    // Ely.by ждёт двухфакторный код прямо в пароле, через двоеточие.
    authenticate(login,
                 password + u':' + totp,
                 clientToken.isEmpty() ? generateClientToken() : clientToken,
                 std::move(callback));
}

void AuthService::authenticate(const QString &login,
                               const QString &password,
                               const QString &clientToken,
                               Callback callback)
{
    if (password.isEmpty()) {
        AuthResult result;
        result.error = tr("Укажите пароль Ely.by в настройках профиля");
        callback(result);
        return;
    }

    emit progress(tr("Авторизация на Ely.by…"));
    postJson(QStringLiteral("/auth/authenticate"),
             {{QStringLiteral("username"), login},
              {QStringLiteral("password"), password},
              {QStringLiteral("clientToken"), clientToken},
              {QStringLiteral("requestUser"), true}},
             [clientToken, callback](int status, const QJsonObject &response, const QString &transportError) {
                 AuthResult result;
                 result.clientToken = clientToken;

                 if (!transportError.isEmpty()) {
                     result.error = AuthService::tr("Нет связи с authserver.ely.by: %1").arg(transportError);
                     callback(result);
                     return;
                 }

                 if (status != 200) {
                     const QString message = describeElyError(response, status);
                     // Единственный случай, когда ошибка лечится вводом кода, а не пароля.
                     result.twoFactorRequired = message.contains(QStringLiteral("two factor"),
                                                                 Qt::CaseInsensitive);
                     result.error = result.twoFactorRequired
                                        ? AuthService::tr("Аккаунт защищён двухфакторной аутентификацией")
                                        : message;
                     callback(result);
                     return;
                 }

                 const QJsonObject profile = response.value(QStringLiteral("selectedProfile")).toObject();
                 result.playerName = profile.value(QStringLiteral("name")).toString();
                 result.uuid = normalizeUuid(profile.value(QStringLiteral("id")).toString());
                 result.accessToken = response.value(QStringLiteral("accessToken")).toString();
                 result.userType = QStringLiteral("ELYBY");
                 result.ok = !result.accessToken.isEmpty() && !result.playerName.isEmpty();
                 if (!result.ok)
                     result.error = AuthService::tr("Ely.by не вернул игровой профиль для этого аккаунта");
                 callback(result);
             });
}

void AuthService::ensureAuthlibInjector(
    const QString &targetDir,
    std::function<void(const QString &, const QString &)> callback)
{
    QDir().mkpath(targetDir);
    const QString jarPath = targetDir + u'/' + kInjectorFileName;
    if (QFileInfo::exists(jarPath)) {
        callback(jarPath, {});
        return;
    }

    emit progress(tr("Загрузка authlib-injector…"));
    QNetworkRequest manifestRequest{QUrl(kInjectorManifest)};
    manifestRequest.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
                                 QNetworkRequest::NoLessSafeRedirectPolicy);

    QNetworkReply *manifestReply = m_network->get(manifestRequest);
    connect(manifestReply, &QNetworkReply::finished, this, [this, manifestReply, jarPath, callback]() {
        manifestReply->deleteLater();
        if (manifestReply->error() != QNetworkReply::NoError) {
            callback({}, tr("Не удалось получить сведения об authlib-injector: %1")
                             .arg(manifestReply->errorString()));
            return;
        }

        const QJsonObject manifest = QJsonDocument::fromJson(manifestReply->readAll()).object();
        const QString url = manifest.value(QStringLiteral("download_url")).toString();
        const QString expectedSha256 = manifest.value(QStringLiteral("checksums"))
                                           .toObject()
                                           .value(QStringLiteral("sha256"))
                                           .toString();
        if (url.isEmpty()) {
            callback({}, tr("В манифесте authlib-injector нет ссылки на файл"));
            return;
        }

        QNetworkRequest jarRequest{QUrl(url)};
        jarRequest.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
                                QNetworkRequest::NoLessSafeRedirectPolicy);
        QNetworkReply *jarReply = m_network->get(jarRequest);
        connect(jarReply, &QNetworkReply::finished, this, [jarReply, jarPath, expectedSha256, callback]() {
            jarReply->deleteLater();
            if (jarReply->error() != QNetworkReply::NoError) {
                callback({}, AuthService::tr("Не удалось скачать authlib-injector: %1").arg(jarReply->errorString()));
                return;
            }

            const QByteArray payload = jarReply->readAll();
            if (!expectedSha256.isEmpty()) {
                const QString actual = QString::fromLatin1(
                    QCryptographicHash::hash(payload, QCryptographicHash::Sha256).toHex());
                if (actual != expectedSha256) {
                    callback({}, AuthService::tr("Контрольная сумма authlib-injector не совпала"));
                    return;
                }
            }

            QFile file(jarPath);
            if (!file.open(QIODevice::WriteOnly) || file.write(payload) != payload.size()) {
                callback({}, AuthService::tr("Не удалось сохранить %1").arg(QDir::toNativeSeparators(jarPath)));
                return;
            }
            file.close();
            callback(jarPath, {});
        });
    });
}
