#include <glib/gi18n.h>
#include <gcrypt.h>
#include "manual-add-dialog.h"
#include "common.h"
#include "db-common.h"
#include "otp-validation.h"
#include "parse-uri.h"
#include "../otpclient-application.h"

struct _ManualAddDialog
{
    AdwDialog parent;

    DatabaseData *db_data;
    ManualAddCallback callback;
    gpointer callback_data;

    GtkWidget *label_row;
    GtkWidget *issuer_row;
    GtkWidget *secret_row;
    GtkWidget *type_combo;
    GtkWidget *algo_combo;
    GtkWidget *digits_spin;
    GtkWidget *period_spin;
    GtkWidget *counter_spin;
    GtkWidget *period_row;
    GtkWidget *counter_row;
    GtkWidget *group_row;
    GtkWidget *add_button;
    GtkWidget *error_label;
    GtkWidget *sha1_banner;

    gboolean applying_uri;  /* re-entrancy guard while populating fields */
};

G_DEFINE_FINAL_TYPE (ManualAddDialog, manual_add_dialog, ADW_TYPE_DIALOG)

static void
update_add_sensitivity (ManualAddDialog *self)
{
    const gchar *label = gtk_editable_get_text (GTK_EDITABLE (self->label_row));
    const gchar *issuer = gtk_editable_get_text (GTK_EDITABLE (self->issuer_row));
    const gchar *secret = gtk_editable_get_text (GTK_EDITABLE (self->secret_row));

    /* A token only needs one name: accept either a label or an issuer. */
    gboolean has_name = (label != NULL && label[0] != '\0') ||
                        (issuer != NULL && issuer[0] != '\0');
    gboolean sensitive = (has_name && secret != NULL && secret[0] != '\0');
    gtk_widget_set_sensitive (self->add_button, sensitive);
}

static gboolean
try_apply_otpauth_uri (ManualAddDialog *self,
                       const gchar     *text)
{
    if (text == NULL || g_ascii_strncasecmp (text, "otpauth://", 10) != 0)
        return FALSE;

    GSList *otps = NULL;
    set_otps_from_uris (text, &otps);
    if (otps == NULL)
        return FALSE;

    otp_t *otp = otps->data;
    if (otp == NULL || otp->secret == NULL || otp->type == NULL) {
        free_otps_gslist (otps, g_slist_length (otps));
        return FALSE;
    }

    self->applying_uri = TRUE;

    gtk_editable_set_text (GTK_EDITABLE (self->secret_row), otp->secret);
    if (otp->account_name != NULL)
        gtk_editable_set_text (GTK_EDITABLE (self->label_row), otp->account_name);
    if (otp->issuer != NULL)
        gtk_editable_set_text (GTK_EDITABLE (self->issuer_row), otp->issuer);

    gboolean is_hotp = (g_ascii_strcasecmp (otp->type, "HOTP") == 0);
    adw_combo_row_set_selected (ADW_COMBO_ROW (self->type_combo), is_hotp ? 1 : 0);

    if (otp->algo != NULL) {
        guint algo_idx = 0;
        if (g_ascii_strcasecmp (otp->algo, "SHA256") == 0) algo_idx = 1;
        else if (g_ascii_strcasecmp (otp->algo, "SHA512") == 0) algo_idx = 2;
        adw_combo_row_set_selected (ADW_COMBO_ROW (self->algo_combo), algo_idx);
    }

    if (otp->digits >= 4 && otp->digits <= 10)
        adw_spin_row_set_value (ADW_SPIN_ROW (self->digits_spin), (double) otp->digits);

    if (is_hotp) {
        adw_spin_row_set_value (ADW_SPIN_ROW (self->counter_spin), (double) otp->counter);
    } else if (otp->period > 0 && otp->period <= 300) {
        adw_spin_row_set_value (ADW_SPIN_ROW (self->period_spin), (double) otp->period);
    }

    free_otps_gslist (otps, g_slist_length (otps));

    self->applying_uri = FALSE;
    return TRUE;
}

static void
on_field_changed (GtkEditable     *editable,
                  ManualAddDialog *self)
{
    (void) editable;
    update_add_sensitivity (self);
    gtk_widget_set_visible (self->error_label, FALSE);
}

static void
on_secret_changed (GtkEditable     *editable,
                   ManualAddDialog *self)
{
    if (self->applying_uri) {
        update_add_sensitivity (self);
        return;
    }

    const gchar *text = gtk_editable_get_text (editable);
    if (text != NULL && g_ascii_strncasecmp (text, "otpauth://", 10) == 0) {
        try_apply_otpauth_uri (self, text);
    }

    update_add_sensitivity (self);
    gtk_widget_set_visible (self->error_label, FALSE);
}

static void
on_type_changed (AdwComboRow     *combo_row,
                 GParamSpec      *pspec,
                 ManualAddDialog *self)
{
    (void) pspec;
    guint selected = adw_combo_row_get_selected (combo_row);

    /* 0 = TOTP, 1 = HOTP */
    gtk_widget_set_visible (self->period_row, selected == 0);
    gtk_widget_set_visible (self->counter_row, selected == 1);
}

static void
on_algo_changed (AdwComboRow     *combo_row,
                 GParamSpec      *pspec,
                 ManualAddDialog *self)
{
    (void) pspec;
    guint selected = adw_combo_row_get_selected (combo_row);
    /* 0 = SHA1, show warning */
    adw_banner_set_revealed (ADW_BANNER (self->sha1_banner), selected == 0);
}

static void
on_add_clicked (GtkButton       *button,
                ManualAddDialog *self)
{
    (void) button;

    GApplication *default_app = g_application_get_default ();
    OTPClientApplication *app = OTPCLIENT_IS_APPLICATION (default_app)
        ? OTPCLIENT_APPLICATION (default_app)
        : NULL;
    if (app == NULL || otpclient_application_get_app_locked (app) ||
        otpclient_application_get_db_data (app) != self->db_data)
    {
        gtk_label_set_text (GTK_LABEL (self->error_label),
                            _("The active database changed. Reopen this dialog before saving."));
        gtk_widget_set_visible (self->error_label, TRUE);
        return;
    }

    const gchar *label_text = gtk_editable_get_text (GTK_EDITABLE (self->label_row));
    const gchar *issuer = gtk_editable_get_text (GTK_EDITABLE (self->issuer_row));
    const gchar *secret = gtk_editable_get_text (GTK_EDITABLE (self->secret_row));

    guint type_idx = adw_combo_row_get_selected (ADW_COMBO_ROW (self->type_combo));
    const gchar *type = (type_idx == 0) ? "TOTP" : "HOTP";

    guint algo_idx = adw_combo_row_get_selected (ADW_COMBO_ROW (self->algo_combo));
    const gchar *algo;
    switch (algo_idx)
    {
        case 1: algo = "SHA256"; break;
        case 2: algo = "SHA512"; break;
        default: algo = "SHA1"; break;
    }

    double digits_d = adw_spin_row_get_value (ADW_SPIN_ROW (self->digits_spin));
    guint digits = (guint) digits_d;
    double period_d = adw_spin_row_get_value (ADW_SPIN_ROW (self->period_spin));
    guint period = (guint) period_d;
    double counter_d = adw_spin_row_get_value (ADW_SPIN_ROW (self->counter_spin));
    guint64 counter = (guint64) counter_d;

    const gchar *group_text = gtk_editable_get_text (GTK_EDITABLE (self->group_row));
    const gchar *group = (group_text != NULL && group_text[0] != '\0') ? group_text : NULL;

    otp_t otp = {
        .type = (gchar *) type,
        .account_name = (gchar *) label_text,
        .issuer = (gchar *) issuer,
        .secret = (gchar *) secret,
        .digits = digits,
        .algo = (gchar *) algo,
        .group = (gchar *) group,
    };
    if (g_ascii_strcasecmp (type, "HOTP") == 0)
        otp.counter = counter;
    else
        otp.period = period;
    GSList otp_list = {
        .data = &otp,
        .next = NULL,
    };

    GError *err = NULL;
    if (!otp_validate_import_token (&otp, &err))
    {
        gtk_label_set_text (GTK_LABEL (self->error_label), err->message);
        gtk_widget_set_visible (self->error_label, TRUE);
        g_clear_error (&err);
        return;
    }

    OtpImportReport report = {0, 0, 0};
    db_import_otps (self->db_data, &otp_list, &report, &err);
    if (err != NULL)
    {
        gtk_label_set_text (GTK_LABEL (self->error_label), err->message);
        gtk_widget_set_visible (self->error_label, TRUE);
        g_clear_error (&err);
        return;
    }

    if (report.added == 0)
    {
        gtk_label_set_text (GTK_LABEL (self->error_label),
                            report.skipped_duplicates > 0
                                ? _("This token already exists")
                                : _("Token data is invalid"));
        gtk_widget_set_visible (self->error_label, TRUE);
        return;
    }

    adw_dialog_close (ADW_DIALOG (self));

    if (self->callback != NULL)
        self->callback (self->callback_data);
}

static void
manual_add_dialog_dispose (GObject *object)
{
    ManualAddDialog *self = MANUAL_ADD_DIALOG (object);
    g_clear_pointer (&self->db_data, database_data_free);

    G_OBJECT_CLASS (manual_add_dialog_parent_class)->dispose (object);
}

static void
manual_add_dialog_init (ManualAddDialog *self)
{
    (void) self;
}

static void
manual_add_dialog_class_init (ManualAddDialogClass *klass)
{
    GObjectClass *object_class = G_OBJECT_CLASS (klass);
    object_class->dispose = manual_add_dialog_dispose;
}

ManualAddDialog *
manual_add_dialog_new (DatabaseData      *db_data,
                       ManualAddCallback  callback,
                       gpointer           user_data)
{
    ManualAddDialog *self = g_object_new (MANUAL_ADD_TYPE_DIALOG,
                                          "title", _("Add Token"),
                                          "content-width", 480,
                                          "content-height", 700,
                                          NULL);

    self->db_data = database_data_ref (db_data);
    self->callback = callback;
    self->callback_data = user_data;

    /* Build UI */
    GtkWidget *toolbar_view = adw_toolbar_view_new ();
    GtkWidget *header = adw_header_bar_new ();
    adw_toolbar_view_add_top_bar (ADW_TOOLBAR_VIEW (toolbar_view), header);

    GtkWidget *clamp = adw_clamp_new ();
    gtk_widget_set_margin_start (clamp, 12);
    gtk_widget_set_margin_end (clamp, 12);
    gtk_widget_set_margin_top (clamp, 12);
    gtk_widget_set_margin_bottom (clamp, 12);

    GtkWidget *box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 18);
    adw_clamp_set_child (ADW_CLAMP (clamp), box);

    /* Account details group */
    GtkWidget *details_group = adw_preferences_group_new ();
    adw_preferences_group_set_title (ADW_PREFERENCES_GROUP (details_group), _("Account Details"));

    self->label_row = adw_entry_row_new ();
    adw_preferences_row_set_title (ADW_PREFERENCES_ROW (self->label_row), _("Label"));
    g_signal_connect (self->label_row, "changed", G_CALLBACK (on_field_changed), self);
    adw_preferences_group_add (ADW_PREFERENCES_GROUP (details_group), self->label_row);

    self->issuer_row = adw_entry_row_new ();
    adw_preferences_row_set_title (ADW_PREFERENCES_ROW (self->issuer_row), _("Issuer"));
    g_signal_connect (self->issuer_row, "changed", G_CALLBACK (on_field_changed), self);
    adw_preferences_group_add (ADW_PREFERENCES_GROUP (details_group), self->issuer_row);

    self->group_row = adw_entry_row_new ();
    adw_preferences_row_set_title (ADW_PREFERENCES_ROW (self->group_row), _("Group"));
    adw_preferences_group_add (ADW_PREFERENCES_GROUP (details_group), self->group_row);

    self->secret_row = adw_entry_row_new ();
    adw_preferences_row_set_title (ADW_PREFERENCES_ROW (self->secret_row), _("Secret (Base32) or otpauth:// URI"));
    gtk_widget_set_tooltip_text (self->secret_row,
        _("Paste the Base32 secret from your provider, or a full otpauth:// URI to autofill every field."));
    g_signal_connect (self->secret_row, "changed", G_CALLBACK (on_secret_changed), self);
    adw_preferences_group_add (ADW_PREFERENCES_GROUP (details_group), self->secret_row);

    gtk_box_append (GTK_BOX (box), details_group);

    /* Token settings group */
    GtkWidget *settings_group = adw_preferences_group_new ();
    adw_preferences_group_set_title (ADW_PREFERENCES_GROUP (settings_group), _("Token Settings"));

    /* Type dropdown */
    const char * const type_items[] = { "TOTP", "HOTP", NULL };
    GtkStringList *type_model = gtk_string_list_new (type_items);
    self->type_combo = adw_combo_row_new ();
    adw_preferences_row_set_title (ADW_PREFERENCES_ROW (self->type_combo), _("Type"));
    adw_combo_row_set_model (ADW_COMBO_ROW (self->type_combo), G_LIST_MODEL (type_model));
    g_signal_connect (self->type_combo, "notify::selected", G_CALLBACK (on_type_changed), self);
    adw_preferences_group_add (ADW_PREFERENCES_GROUP (settings_group), self->type_combo);

    /* Algorithm dropdown */
    const char * const algo_items[] = { "SHA1", "SHA256", "SHA512", NULL };
    GtkStringList *algo_model = gtk_string_list_new (algo_items);
    self->algo_combo = adw_combo_row_new ();
    adw_preferences_row_set_title (ADW_PREFERENCES_ROW (self->algo_combo), _("Algorithm"));
    adw_combo_row_set_model (ADW_COMBO_ROW (self->algo_combo), G_LIST_MODEL (algo_model));
    gtk_widget_set_tooltip_text (self->algo_combo,
        _("HMAC hash function. Must match what the provider expects - most use SHA1."));
    g_signal_connect (self->algo_combo, "notify::selected", G_CALLBACK (on_algo_changed), self);
    adw_preferences_group_add (ADW_PREFERENCES_GROUP (settings_group), self->algo_combo);

    /* Digits */
    GtkAdjustment *digits_adj = gtk_adjustment_new (6, OTP_DIGITS_MIN, OTP_DIGITS_MAX, 1, 1, 0);
    self->digits_spin = adw_spin_row_new (digits_adj, 1, 0);
    adw_preferences_row_set_title (ADW_PREFERENCES_ROW (self->digits_spin), _("Digits"));
    gtk_widget_set_tooltip_text (self->digits_spin,
        _("Number of digits in the generated code. Most providers use 6."));
    adw_preferences_group_add (ADW_PREFERENCES_GROUP (settings_group), self->digits_spin);

    /* Period (TOTP) */
    GtkAdjustment *period_adj = gtk_adjustment_new (30, OTP_PERIOD_MIN, OTP_PERIOD_MAX, 1, 10, 0);
    self->period_spin = adw_spin_row_new (period_adj, 1, 0);
    adw_preferences_row_set_title (ADW_PREFERENCES_ROW (self->period_spin), _("Period (seconds)"));
    gtk_widget_set_tooltip_text (self->period_spin,
        _("How often a TOTP code rotates. Standard is 30 seconds."));
    self->period_row = self->period_spin;
    adw_preferences_group_add (ADW_PREFERENCES_GROUP (settings_group), self->period_spin);

    /* Counter (HOTP) */
    GtkAdjustment *counter_adj = gtk_adjustment_new (
        0, 0, (double) (OTP_HOTP_COUNTER_MAX - 1), 1, 10, 0);
    self->counter_spin = adw_spin_row_new (counter_adj, 1, 0);
    adw_preferences_row_set_title (ADW_PREFERENCES_ROW (self->counter_spin), _("Counter"));
    gtk_widget_set_tooltip_text (self->counter_spin,
        _("Initial HOTP counter value. Must match the value the provider expects."));
    self->counter_row = self->counter_spin;
    gtk_widget_set_visible (self->counter_row, FALSE);
    adw_preferences_group_add (ADW_PREFERENCES_GROUP (settings_group), self->counter_spin);

    gtk_box_append (GTK_BOX (box), settings_group);

    /* SHA1 warning banner */
    self->sha1_banner = adw_banner_new (_("SHA1 is considered weak. Use SHA256 or SHA512 if your provider supports it."));
    adw_banner_set_revealed (ADW_BANNER (self->sha1_banner), TRUE);
    gtk_box_append (GTK_BOX (box), self->sha1_banner);

    /* Error label */
    self->error_label = gtk_label_new (NULL);
    gtk_widget_add_css_class (self->error_label, "error");
    gtk_widget_set_visible (self->error_label, FALSE);
    gtk_box_append (GTK_BOX (box), self->error_label);

    /* Add button */
    self->add_button = gtk_button_new_with_label (_("Add Token"));
    gtk_widget_add_css_class (self->add_button, "suggested-action");
    gtk_widget_add_css_class (self->add_button, "pill");
    gtk_widget_set_halign (self->add_button, GTK_ALIGN_CENTER);
    gtk_widget_set_sensitive (self->add_button, FALSE);
    g_signal_connect (self->add_button, "clicked", G_CALLBACK (on_add_clicked), self);
    gtk_box_append (GTK_BOX (box), self->add_button);

    GtkWidget *scrolled = gtk_scrolled_window_new ();
    gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled),
                                    GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
    gtk_scrolled_window_set_propagate_natural_height (GTK_SCROLLED_WINDOW (scrolled), TRUE);
    gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scrolled), clamp);
    gtk_widget_set_vexpand (scrolled, TRUE);
    adw_toolbar_view_set_content (ADW_TOOLBAR_VIEW (toolbar_view), scrolled);
    adw_dialog_set_child (ADW_DIALOG (self), toolbar_view);

    return self;
}
