#include "gui/license_manager.hpp"

#include <QDateTime>
#include <QDialog>
#include <QDialogButtonBox>
#include <QDir>
#include <QFile>
#include <QFileDialog>
#include <QFileInfo>
#include <QFormLayout>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QJsonDocument>
#include <QJsonObject>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QPushButton>
#include <QSaveFile>
#include <QSslSocket>
#include <QUrl>
#include <QVBoxLayout>

#include "core/license_service_config.hpp"

#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#include <wincrypt.h>
#endif

namespace smartcut::gui {
namespace {

constexpr qint64 kMaximumLicenseBytes = 16 * 1024;
constexpr qint64 kRefreshThresholdSeconds = 7LL * 24LL * 60LL * 60LL;

struct StoredCredentials {
  QString email;
  QString license_key;
};

QString format_timestamp(std::int64_t value, const QString& perpetual = {}) {
  if (value == 0) return perpetual;
  return QDateTime::fromSecsSinceEpoch(value).toLocalTime().toString("yyyy-MM-dd HH:mm:ss t");
}

QString path_to_qstring(const std::filesystem::path& path) {
#ifdef _WIN32
  return QString::fromStdWString(path.wstring());
#else
  const auto utf8 = path.u8string();
  return QString::fromUtf8(reinterpret_cast<const char*>(utf8.data()),
                           static_cast<qsizetype>(utf8.size()));
#endif
}

QString activation_path() {
  const auto entitlement = path_to_qstring(core::default_entitlement_path());
  return entitlement.isEmpty() ? QString{} : QFileInfo(entitlement).dir().filePath("online-activation.dat");
}

QString configured_service_url() {
#ifdef SMARTCUT_INTERNAL_TEST_GUI
  const auto override_url = qEnvironmentVariable("SMARTCUT64_LICENSE_SERVICE_URL").trimmed();
  return override_url.isEmpty() ? QString::fromUtf8(SMARTCUT_LICENSE_SERVICE_URL).trimmed()
                                : override_url;
#else
  return QString::fromUtf8(SMARTCUT_LICENSE_SERVICE_URL).trimmed();
#endif
}

bool trusted_service_url(const QUrl& url) {
  if (!url.isValid() || url.host().isEmpty()) return false;
#ifdef SMARTCUT_INTERNAL_TEST_GUI
  if (url.scheme().compare("https", Qt::CaseInsensitive) == 0) return true;
  if (url.scheme().compare("http", Qt::CaseInsensitive) != 0) return false;
  const auto host = url.host().toLower();
  return host == "localhost" || host == "127.0.0.1" || host == "::1";
#else
  const QUrl configured(configured_service_url());
  return configured.isValid() && configured.scheme().compare("https", Qt::CaseInsensitive) == 0 &&
         url.scheme().compare("https", Qt::CaseInsensitive) == 0 &&
         url.host().compare(configured.host(), Qt::CaseInsensitive) == 0 &&
         url.port(-1) == configured.port(-1) && url.userInfo().isEmpty();
#endif
}

QUrl service_endpoint(const QString& path) {
  auto base = configured_service_url();
  while (base.endsWith('/')) base.chop(1);
  const QUrl url(base + path);
  return trusted_service_url(url) ? url : QUrl{};
}

QByteArray protect_credentials(const QByteArray& plain) {
#ifndef _WIN32
  Q_UNUSED(plain);
  return {};
#else
  QByteArray entropy("SmartCut64 online activation v1");
  DATA_BLOB input{static_cast<DWORD>(plain.size()),
                  reinterpret_cast<BYTE*>(const_cast<char*>(plain.constData()))};
  DATA_BLOB extra{static_cast<DWORD>(entropy.size()),
                  reinterpret_cast<BYTE*>(entropy.data())};
  DATA_BLOB output{};
  if (!CryptProtectData(&input, L"SmartCut64 online activation", &extra, nullptr, nullptr,
                        CRYPTPROTECT_UI_FORBIDDEN, &output))
    return {};
  const QByteArray protected_bytes(reinterpret_cast<const char*>(output.pbData),
                                   static_cast<qsizetype>(output.cbData));
  LocalFree(output.pbData);
  return protected_bytes;
#endif
}

QByteArray unprotect_credentials(const QByteArray& protected_bytes) {
#ifndef _WIN32
  Q_UNUSED(protected_bytes);
  return {};
#else
  QByteArray entropy("SmartCut64 online activation v1");
  DATA_BLOB input{static_cast<DWORD>(protected_bytes.size()),
                  reinterpret_cast<BYTE*>(const_cast<char*>(protected_bytes.constData()))};
  DATA_BLOB extra{static_cast<DWORD>(entropy.size()),
                  reinterpret_cast<BYTE*>(entropy.data())};
  DATA_BLOB output{};
  if (!CryptUnprotectData(&input, nullptr, &extra, nullptr, nullptr,
                          CRYPTPROTECT_UI_FORBIDDEN, &output))
    return {};
  const QByteArray plain(reinterpret_cast<const char*>(output.pbData),
                         static_cast<qsizetype>(output.cbData));
  LocalFree(output.pbData);
  return plain;
#endif
}

bool save_credentials(const StoredCredentials& credentials) {
  const auto path = activation_path();
  if (path.isEmpty() || credentials.email.isEmpty() || credentials.license_key.isEmpty()) return false;
  const QJsonObject object{{"schema", "smartcut64-online-activation-v1"},
                           {"email", credentials.email},
                           {"license_key", credentials.license_key},
                           {"installation_id", QString::fromStdString(core::current_installation_id())}};
  const auto protected_bytes = protect_credentials(QJsonDocument(object).toJson(QJsonDocument::Compact));
  if (protected_bytes.isEmpty() || !QDir().mkpath(QFileInfo(path).absolutePath())) return false;
  QSaveFile output(path);
  return output.open(QIODevice::WriteOnly) && output.write(protected_bytes) == protected_bytes.size() &&
         output.commit();
}

bool load_credentials(StoredCredentials& credentials) {
  QFile input(activation_path());
  if (!input.open(QIODevice::ReadOnly)) return false;
  const auto protected_bytes = input.read(32 * 1024);
  if (input.error() != QFileDevice::NoError || !input.atEnd()) return false;
  const auto document = QJsonDocument::fromJson(unprotect_credentials(protected_bytes));
  if (!document.isObject()) return false;
  const auto object = document.object();
  if (object.value("schema").toString() != "smartcut64-online-activation-v1" ||
      object.value("installation_id").toString() != QString::fromStdString(core::current_installation_id()))
    return false;
  credentials.email = object.value("email").toString().trimmed().toLower();
  credentials.license_key = object.value("license_key").toString().trimmed();
  return !credentials.email.isEmpty() && !credentials.license_key.isEmpty();
}

using OnlineCompletion = std::function<void(bool, const QJsonObject&, const QString&)>;

void post_online(QObject* context, const QString& path, const QJsonObject& body,
                 OnlineCompletion completion) {
  const auto endpoint = service_endpoint(path);
  if (endpoint.isEmpty()) {
    completion(false, {}, "Online activation is not configured in this build.");
    return;
  }
  if (endpoint.scheme().compare("https", Qt::CaseInsensitive) == 0 &&
      !QSslSocket::supportsSsl()) {
    completion(false, {},
               "Secure HTTPS support is missing from this installation. Install the latest "
               "SmartCut64 package and try again.");
    return;
  }
  auto* network = new QNetworkAccessManager(context);
  QNetworkRequest request(endpoint);
  request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
  request.setRawHeader("Accept", "application/json");
  request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
                       QNetworkRequest::ManualRedirectPolicy);
  request.setTransferTimeout(15000);
  auto* reply = network->post(request, QJsonDocument(body).toJson(QJsonDocument::Compact));
  QObject::connect(reply, &QNetworkReply::finished, context,
                   [network, reply, completion = std::move(completion)]() mutable {
    const auto status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
    const auto bytes = reply->read(32 * 1024);
    const auto document = QJsonDocument::fromJson(bytes);
    const auto object = document.isObject() ? document.object() : QJsonObject{};
    const bool success = reply->error() == QNetworkReply::NoError && status >= 200 &&
                         status < 300 && object.value("ok").toBool();
    auto message = object.value("message").toString();
    if (!success && message.isEmpty()) {
      if (reply->error() == QNetworkReply::TimeoutError) {
        message = "The activation service timed out. Please try again.";
      } else if (reply->error() == QNetworkReply::SslHandshakeFailedError) {
        message = "A secure connection to the activation service could not be established. "
                  "Check the Windows date and time, then try again.";
      } else if (reply->error() == QNetworkReply::HostNotFoundError) {
        message = "The activation service address could not be resolved. Check the Internet or "
                  "DNS connection and try again.";
      } else {
        message = "The activation service could not be reached. Please try again.";
      }
    }
    completion(success, object, message);
    reply->deleteLater();
    network->deleteLater();
  });
}

QJsonObject activation_request(const StoredCredentials& credentials) {
  return {{"license_key", credentials.license_key},
          {"email", credentials.email},
          {"installation_id", QString::fromStdString(core::current_installation_id())}};
}

}  // namespace

LicenseManager::LicenseManager() { reload(); }

QString LicenseManager::default_path() const {
  return path_to_qstring(core::default_entitlement_path());
}

QString LicenseManager::configured_path() const {
  return path_to_qstring(core::installed_entitlement_path());
}

void LicenseManager::reload() {
  active_path_ = configured_path();
  result_ = core::verify_installed_entitlement(QDateTime::currentSecsSinceEpoch());
}

bool LicenseManager::online_available() const {
  return !service_endpoint("/health").isEmpty();
}

QString LicenseManager::edition_suffix() const {
  return paid() ? QString{} : QStringLiteral(" [Evaluation - watermarked exports]");
}

QString LicenseManager::status_text() const {
  if (paid()) {
    if (result_.expires_unix == 0)
      return QString("Paid perpetual licence - %1").arg(QString::fromStdString(result_.customer));
    return QString("Paid licence - %1 - valid until %2")
        .arg(QString::fromStdString(result_.customer), format_timestamp(result_.expires_unix));
  }
  return QString("Evaluation mode - clean export unavailable (%1)")
      .arg(QString::fromStdString(result_.detail));
}

bool LicenseManager::install_token(const QByteArray& raw_token, QString& error) {
  if (raw_token.size() > kMaximumLicenseBytes) {
    error = "The licence response is too large.";
    return false;
  }
  const auto token = raw_token.trimmed();
  const auto candidate = core::verify_entitlement(token.toStdString(),
                                                   QDateTime::currentSecsSinceEpoch());
  if (!candidate.paid()) {
    error = QString::fromStdString(candidate.detail);
    return false;
  }
  if (!qEnvironmentVariable("SMARTCUT64_LICENSE_FILE").trimmed().isEmpty()) {
    error = "Online or file activation is disabled while the SMARTCUT64_LICENSE_FILE test override is set.";
    return false;
  }
  const auto destination = configured_path();
  if (destination.isEmpty() || !QDir().mkpath(QFileInfo(destination).absolutePath())) {
    error = "The licence folder could not be created.";
    return false;
  }
  QSaveFile output(destination);
  if (!output.open(QIODevice::WriteOnly) || output.write(token) != token.size() ||
      output.write("\n", 1) != 1 || !output.commit()) {
    error = "The licence could not be installed.";
    return false;
  }
  reload();
  if (!paid()) error = QString::fromStdString(result_.detail);
  return paid();
}

bool LicenseManager::install_file(const QString& source, QWidget* parent) {
  QFile input(source);
  if (!input.open(QIODevice::ReadOnly)) {
    QMessageBox::critical(parent, "SmartCut64 Licence", "The selected licence file could not be read.");
    return false;
  }
  const auto token = input.read(kMaximumLicenseBytes + 1);
  const bool invalid = token.size() > kMaximumLicenseBytes || !input.atEnd() ||
                       input.error() != QFileDevice::NoError;
  input.close();
  QString error;
  if (invalid || !install_token(token, error)) {
    QMessageBox::critical(parent, "SmartCut64 Licence",
                          invalid ? "The selected licence file is invalid or too large."
                                  : QString("The licence was rejected.\n\n%1").arg(error));
    return false;
  }
  return true;
}

void LicenseManager::refresh_online_async(QObject* context, RefreshCompletion completion) {
  reload();
  const auto now = QDateTime::currentSecsSinceEpoch();
  if (paid() && (result_.expires_unix == 0 || result_.expires_unix - now > kRefreshThresholdSeconds)) {
    if (completion) completion(false, {});
    return;
  }
  StoredCredentials credentials;
  if (!online_available() || !load_credentials(credentials)) {
    if (completion) completion(false, {});
    return;
  }
  const bool was_paid = paid();
  post_online(context, "/v1/activate", activation_request(credentials),
              [this, was_paid, completion = std::move(completion)](
                  bool success, const QJsonObject& object, const QString& message) mutable {
    QString install_error;
    const bool installed = success &&
        install_token(object.value("entitlement").toString().toUtf8(), install_error);
    if (completion) {
      if (installed) completion(true, "Paid licence verified securely");
      else if (!was_paid) completion(false, message.isEmpty() ? install_error : message);
      else completion(false, {});
    }
  });
}

void LicenseManager::show_dialog(QWidget* parent) {
  reload();
  QDialog dialog(parent);
  dialog.setWindowTitle("SmartCut64 Licence & Activation");
  dialog.setMinimumWidth(600);
  auto* layout = new QVBoxLayout(&dialog);
  auto* heading = new QLabel(paid() ? "Paid licence verified" : "Evaluation mode", &dialog);
  heading->setStyleSheet(paid() ? "font-size:16px;font-weight:700;color:#16721f;"
                                : "font-size:16px;font-weight:700;color:#a52a2a;");
  layout->addWidget(heading);
  auto* explanation = new QLabel(
      paid() ? "This installation may create clean video exports. The licence signature is checked locally."
             : "Editing and preview remain available. Every video export is permanently watermarked and "
               "requires a full video re-encode until a valid paid licence is installed.", &dialog);
  explanation->setWordWrap(true);
  layout->addWidget(explanation);

  auto* form = new QFormLayout;
  form->addRow("Status:", new QLabel(QString::fromLatin1(core::entitlement_state_name(result_.state)), &dialog));
  form->addRow("Customer:", new QLabel(QString::fromStdString(result_.customer), &dialog));
  form->addRow("Licence ID:", new QLabel(QString::fromStdString(result_.license_id), &dialog));
  form->addRow("Issued:", new QLabel(format_timestamp(result_.issued_unix, "-"), &dialog));
  form->addRow("Valid from:", new QLabel(format_timestamp(result_.not_before_unix, "-"), &dialog));
  form->addRow("Expires:", new QLabel(format_timestamp(result_.expires_unix, "Perpetual"), &dialog));
  auto* installation = new QLabel(QString::fromStdString(core::current_installation_id()), &dialog);
  installation->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
  form->addRow("Installation ID:", installation);
  form->addRow("Licence file:", new QLabel(QDir::toNativeSeparators(active_path_), &dialog));
  layout->addLayout(form);

  StoredCredentials stored;
  const bool has_online_activation = load_credentials(stored);
  auto* online = new QGroupBox("Online purchase activation", &dialog);
  auto* online_layout = new QFormLayout(online);
  auto* email = new QLineEdit(online);
  email->setPlaceholderText("Email used for purchase");
  if (has_online_activation) email->setText(stored.email);
  auto* key = new QLineEdit(online);
  key->setPlaceholderText("Lemon Squeezy licence key");
  key->setEchoMode(QLineEdit::Password);
  auto* online_status = new QLabel(online_available()
      ? "The purchase email, licence key and one-way Installation ID are sent securely to the SmartCut64 activation service."
      : "Online activation is not configured in this build. You can still install an issued .sc64 file.", online);
  online_status->setWordWrap(true);
  auto* activate = new QPushButton("Activate This Computer", online);
  auto* deactivate = new QPushButton("Deactivate This Computer", online);
  activate->setEnabled(online_available());
  deactivate->setEnabled(online_available() && has_online_activation);
  auto* online_actions = new QHBoxLayout;
  online_actions->addWidget(activate);
  online_actions->addWidget(deactivate);
  online_actions->addStretch();
  online_layout->addRow("Purchase email:", email);
  online_layout->addRow("Licence key:", key);
  online_layout->addRow(online_status);
  online_layout->addRow(online_actions);
  layout->addWidget(online);

  auto* detail = new QLabel(QString::fromStdString(result_.detail), &dialog);
  detail->setWordWrap(true);
  layout->addWidget(detail);
  auto* rights = new QLabel(
      "The paid entitlement controls clean exports in official SmartCut64 builds. It does not alter or "
      "restrict the rights granted by the GNU GPL to inspect, modify, build, or redistribute the source.", &dialog);
  rights->setWordWrap(true);
  layout->addWidget(rights);

  auto* actions = new QHBoxLayout;
  auto* install = new QPushButton("Install Licence File...", &dialog);
  auto* remove = new QPushButton("Remove Installed Licence", &dialog);
  remove->setEnabled(QFileInfo::exists(default_path()) && !has_online_activation &&
                     qEnvironmentVariable("SMARTCUT64_LICENSE_FILE").trimmed().isEmpty());
  actions->addWidget(install);
  actions->addWidget(remove);
  actions->addStretch();
  layout->addLayout(actions);
  auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close, &dialog);
  layout->addWidget(buttons);
  QObject::connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);

  QObject::connect(activate, &QPushButton::clicked, &dialog,
                   [this, email, key, activate, deactivate, online_status, &dialog] {
    StoredCredentials credentials{email->text().trimmed().toLower(), key->text().trimmed()};
    if (credentials.email.isEmpty() || credentials.license_key.isEmpty()) {
      online_status->setText("Enter both the purchase email and licence key.");
      return;
    }
    activate->setEnabled(false);
    deactivate->setEnabled(false);
    online_status->setText("Verifying purchase and activating this computer...");
    post_online(&dialog, "/v1/activate", activation_request(credentials),
                [this, credentials, activate, deactivate, online_status, &dialog](
                    bool success, const QJsonObject& object, const QString& message) {
      QString error;
      if (!success || !install_token(object.value("entitlement").toString().toUtf8(), error)) {
        online_status->setText(message.isEmpty() ? error : message);
        activate->setEnabled(true);
        deactivate->setEnabled(false);
        return;
      }
      if (!save_credentials(credentials)) {
        QFile::remove(default_path());
        reload();
        online_status->setText("The purchase was verified, but secure local activation storage failed.");
        activate->setEnabled(true);
        return;
      }
      QMessageBox::information(&dialog, "SmartCut64 Licence",
                               "This computer is activated. Clean smart-render exports are enabled.");
      dialog.accept();
    });
  });

  QObject::connect(deactivate, &QPushButton::clicked, &dialog,
                   [this, stored, activate, deactivate, online_status, &dialog] {
    if (QMessageBox::question(&dialog, "Deactivate SmartCut64",
          "Deactivate this computer and return it to evaluation mode? This releases one purchase activation.",
          QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes) return;
    activate->setEnabled(false);
    deactivate->setEnabled(false);
    online_status->setText("Deactivating this computer...");
    post_online(&dialog, "/v1/deactivate", activation_request(stored),
                [this, activate, deactivate, online_status, &dialog](
                    bool success, const QJsonObject&, const QString& message) {
      if (!success) {
        online_status->setText(message);
        activate->setEnabled(true);
        deactivate->setEnabled(true);
        return;
      }
      QFile::remove(default_path());
      QFile::remove(activation_path());
      reload();
      QMessageBox::information(&dialog, "SmartCut64 Licence",
                               "This computer was deactivated and the activation was released.");
      dialog.accept();
    });
  });

  QObject::connect(install, &QPushButton::clicked, &dialog, [this, parent, &dialog] {
    const auto path = QFileDialog::getOpenFileName(parent, "Install SmartCut64 Licence", {},
                                                    "SmartCut64 Licence (*.sc64);;All Files (*)");
    if (!path.isEmpty() && install_file(path, parent)) {
      QMessageBox::information(parent, "SmartCut64 Licence",
                               "The paid licence was verified and installed. Future video exports will be clean.");
      dialog.accept();
    }
  });
  QObject::connect(remove, &QPushButton::clicked, &dialog, [this, parent, &dialog] {
    if (QMessageBox::question(parent, "Remove SmartCut64 Licence",
                              "Return this installation to evaluation mode? Future video exports will be watermarked.",
                              QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes)
      return;
    if (!QFile::remove(default_path())) {
      QMessageBox::critical(parent, "SmartCut64 Licence", "The installed licence could not be removed.");
      return;
    }
    reload();
    dialog.accept();
  });
  dialog.exec();
}

}  // namespace smartcut::gui
