//  gnoMint: a graphical interface for managing a certification authority
//  Copyright (C) 2006-2009 David Marín Carreño <davefx@gmail.com>
//
//  This file is part of gnoMint.
//
//  gnoMint 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; either version 3 of the License, or   
//  (at your option) any later version.
//
//  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, write to the Free Software
//  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

#include <config.h>

#include <glib-object.h>
#include <gtk/gtk.h>
#include <libintl.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#include "wizard_window.h"
#include "ca_file.h"
#include "ca.h"
#include "tls.h"
#include "pkey_manage.h"
#include "dialog.h"
#include "gnomint_time.h"
#include "new_cert.h"

#include <glib/gi18n.h>

GtkBuilder * wizard_window_gtkb = NULL;
static WizardCertType current_wizard_type = WIZARD_CERT_TYPE_WEB_SERVER;
static GArray *ca_ids = NULL;  /* parallel array of guint64 CA IDs for signing_ca_combo */

typedef struct {
    guint64 ca_id;
    gchar *subject;
    gint64 expiration;
} CAInfo;

/* sqlite3 callback used by ca_file_foreach_ca (signature dictated by
 * CaFileCallbackFunc). Collects each row into the GList passed via
 * user_data. ca_file_foreach_ca already filters is_ca=1 AND
 * revocation IS NULL — see src/ca_file.c. */
static int
__wizard_collect_ca_cb (void *pArg, int argc, char **argv,
                        char **columnNames G_GNUC_UNUSED)
{
    GList **list = (GList **) pArg;
    if (argc > CA_FILE_CA_COLUMN_EXPIRATION &&
        argv[CA_FILE_CA_COLUMN_ID] && argv[CA_FILE_CA_COLUMN_SUBJECT]) {
        CAInfo *ca = g_new0 (CAInfo, 1);
        ca->ca_id = g_ascii_strtoull (argv[CA_FILE_CA_COLUMN_ID], NULL, 10);
        ca->subject = g_strdup (argv[CA_FILE_CA_COLUMN_SUBJECT]);
        ca->expiration = argv[CA_FILE_CA_COLUMN_EXPIRATION]
            ? g_ascii_strtoll (argv[CA_FILE_CA_COLUMN_EXPIRATION], NULL, 10)
            : 0;
        *list = g_list_append (*list, ca);
    }
    return 0;
}

/* sqlite3 callback used by ca_file_foreach_policy. The policy rows
 * are (ca_id, name, value); name/value are argv[1] and argv[2]. */
static int
__wizard_collect_policy_cb (void *pArg, int argc, char **argv,
                            char **columnNames G_GNUC_UNUSED)
{
    GHashTable *table = (GHashTable *) pArg;
    if (argc >= 3 && argv[1] && argv[2]) {
        g_hash_table_insert (table, g_strdup (argv[1]), g_strdup (argv[2]));
    }
    return 0;
}

// Helper function to get CA list (CAs are returned in the same
// hierarchical order ca_file_foreach_ca uses everywhere else in gnoMint).
static GList * __wizard_get_ca_list (void)
{
    GList *ca_list = NULL;
    ca_file_foreach_ca (__wizard_collect_ca_cb, &ca_list);
    return ca_list;
}

// Helper function to get selected CA from main window or first non-expired
static guint64 __wizard_get_default_ca_id (void)
{
    GList *cas = __wizard_get_ca_list ();
    guint64 selected_ca = ca_get_selected_row_id ();
    guint64 result = 0;
    time_t now = time (NULL);

    /* If the user has a non-revoked CA selected in the main view, prefer that. */
    if (selected_ca > 0) {
        for (GList *l = cas; l; l = l->next) {
            CAInfo *ca = l->data;
            if (ca->ca_id == selected_ca) {
                result = selected_ca;
                break;
            }
        }
    }

    /* Otherwise pick the first non-expired CA; if all are expired, the
     * first CA in the list (which is sorted hierarchically). */
    if (result == 0) {
        for (GList *l = cas; l; l = l->next) {
            CAInfo *ca = l->data;
            if (ca->expiration > now) {
                result = ca->ca_id;
                break;
            }
        }
    }
    if (result == 0 && cas) {
        CAInfo *first = cas->data;
        result = first->ca_id;
    }

    for (GList *l = cas; l; l = l->next) {
        CAInfo *ca = l->data;
        g_free (ca->subject);
        g_free (ca);
    }
    g_list_free (cas);
    return result;
}

// Helper function to get CA fields for inheritance
static void __wizard_get_ca_fields (guint64 ca_id, TlsCreationData *creation_data)
{
    // Get CA policy settings
    GHashTable *policy_table = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);

    ca_file_foreach_policy (__wizard_collect_policy_cb, ca_id, policy_table);
    
    // Get CA certificate fields
    gchar *ca_pem = ca_file_get_public_pem_from_id (CA_FILE_ELEMENT_TYPE_CERT, ca_id);
    if (!ca_pem) {
        g_hash_table_destroy (policy_table);
        return;
    }
    
    TlsCert *ca_cert = tls_parse_cert_pem (ca_pem);
    g_free (ca_pem);
    
    if (!ca_cert) {
        g_hash_table_destroy (policy_table);
        return;
    }
    
    // Check and apply inheritance for each field
    gchar *strvalue;
    gint inherit_value;
    
    // Country (C)
    strvalue = (gchar *)g_hash_table_lookup (policy_table, "C_INHERIT");
    inherit_value = strvalue ? atoi(strvalue) : 0;
    if (inherit_value && ca_cert->c) {
        g_free (creation_data->country);
        creation_data->country = g_strdup (ca_cert->c);
    }
    
    // State (ST)
    strvalue = (gchar *)g_hash_table_lookup (policy_table, "ST_INHERIT");
    inherit_value = strvalue ? atoi(strvalue) : 0;
    if (inherit_value && ca_cert->st) {
        g_free (creation_data->state);
        creation_data->state = g_strdup (ca_cert->st);
    }
    
    // Locality (L)
    strvalue = (gchar *)g_hash_table_lookup (policy_table, "L_INHERIT");
    inherit_value = strvalue ? atoi(strvalue) : 0;
    if (inherit_value && ca_cert->l) {
        g_free (creation_data->city);
        creation_data->city = g_strdup (ca_cert->l);
    }
    
    // Organization (O)
    strvalue = (gchar *)g_hash_table_lookup (policy_table, "O_INHERIT");
    inherit_value = strvalue ? atoi(strvalue) : 0;
    if (inherit_value && ca_cert->o) {
        g_free (creation_data->org);
        creation_data->org = g_strdup (ca_cert->o);
    }
    
    // Organizational Unit (OU)
    strvalue = (gchar *)g_hash_table_lookup (policy_table, "OU_INHERIT");
    inherit_value = strvalue ? atoi(strvalue) : 0;
    if (inherit_value && ca_cert->ou) {
        g_free (creation_data->ou);
        creation_data->ou = g_strdup (ca_cert->ou);
    }
    
    tls_cert_free (ca_cert);
    g_hash_table_destroy (policy_table);
}

// Helper function to create CSR with default settings
static guint64 __wizard_create_csr (const gchar *server_name, WizardCertType cert_type, guint64 ca_id)
{
    TlsCreationData *creation_data = g_new0 (TlsCreationData, 1);
    gchar *private_key = NULL;
    gnutls_x509_privkey_t *csr_key = NULL;
    gchar *certificate_sign_request = NULL;
    gchar *error_message = NULL;
    TlsCsr *tlscsr = NULL;
    guint64 csr_id = 0;
    
    // Set default values
    creation_data->key_type = 0; // RSA
    creation_data->key_bitlength = 2048; // Default key size
    creation_data->country = g_strdup(""); // Empty country
    creation_data->state = g_strdup(""); // Empty state
    creation_data->city = g_strdup(""); // Empty locality
    creation_data->org = g_strdup(""); // Empty organization
    creation_data->ou = g_strdup(""); // Empty organizational unit
    creation_data->cn = g_strdup(server_name); // Server name as CN
    creation_data->emailAddress = NULL;        // wizard doesn't ask for an email
    creation_data->parent_ca_id_str = g_strdup_printf("'%" G_GUINT64_FORMAT "'", ca_id);

    // Apply CA field inheritance based on CA policy
    __wizard_get_ca_fields (ca_id, creation_data);

    // Generate RSA key pair
    error_message = tls_generate_rsa_keys (creation_data, &private_key, &csr_key);

    if (error_message) {
        dialog_error (g_strdup_printf (_("Key generation failed:\n%s"), error_message));
        g_free (error_message);
        tls_creation_data_free(creation_data);
        return 0;
    }

    // Generate CSR
    error_message = tls_generate_csr (creation_data, csr_key, &certificate_sign_request);

    if (error_message) {
        dialog_error (g_strdup_printf (_("CSR generation failed:\n%s"), error_message));
        g_free (error_message);
        g_free(private_key);
        tls_creation_data_free(creation_data);
        return 0;
    }
    
    // Parse CSR to get DN
    tlscsr = tls_parse_csr_pem (certificate_sign_request);
    
    if (!tlscsr) {
        dialog_error (_("Failed to parse generated CSR."));
        g_free(private_key);
        g_free(certificate_sign_request);
        tls_creation_data_free(creation_data);
        return 0;
    }
    
    // Save CSR to database
    error_message = ca_file_insert_csr (private_key, certificate_sign_request, NULL, &csr_id);
    
    if (error_message) {
        dialog_error (g_strdup_printf (_("Failed to save CSR:\n%s"), error_message));
        g_free (error_message);
        tls_csr_free(tlscsr);
        g_free(private_key);
        g_free(certificate_sign_request);
        tls_creation_data_free(creation_data);
        return 0;
    }
    
    // Cleanup
    tls_csr_free(tlscsr);
    g_free(private_key);
    g_free(certificate_sign_request);
    tls_creation_data_free(creation_data);
    
    return csr_id;
}

static void
__wizard_sign_csr_done_cb (const gchar *error, gpointer user_data)
{
    (void) user_data;
    if (error) {
        dialog_error (g_strdup_printf (_("Failed to sign certificate:\n%s"), error));
        return;
    }
    dialog_info (_("Certificate generated successfully!"));
    dialog_refresh_list();
}

// Button click handlers
static void on_wizard_generate_button_clicked (GtkButton *button, gpointer user_data)
{
    GtkWidget *dialog = GTK_WIDGET(user_data);
    GtkWidget *server_name_entry = GTK_WIDGET(gtk_builder_get_object (wizard_window_gtkb, "server_name_entry"));
    GtkWidget *signing_ca_combo = GTK_WIDGET(gtk_builder_get_object (wizard_window_gtkb, "signing_ca_combo"));
    const gchar *server_name = gtk_editable_get_text(GTK_EDITABLE(server_name_entry));
    
    // Validate server name
    if (!server_name || strlen(server_name) == 0) {
        dialog_error (_("Please enter a server name."));
        return;
    }
    
    // Get selected CA from GtkDropDown + parallel ca_ids array
    guint selected = gtk_drop_down_get_selected (GTK_DROP_DOWN(signing_ca_combo));
    if (selected == GTK_INVALID_LIST_POSITION || !ca_ids || selected >= ca_ids->len) {
        dialog_error (_("Please select a Certificate Authority."));
        return;
    }

    guint64 ca_id = g_array_index (ca_ids, guint64, selected);

    if (ca_id == 0) {
        dialog_error (_("Invalid Certificate Authority selected."));
        return;
    }
    
    // Create CSR
    guint64 csr_id = __wizard_create_csr (server_name, current_wizard_type, ca_id);
    if (csr_id == 0) {
        return; // Error already shown
    }
    
    // Prepare certificate creation data
    TlsCertCreationData *cert_creation_data = g_new0 (TlsCertCreationData, 1);
    cert_creation_data->key_months_before_expiration = 12; // 1 year default
    
    // Generated certificates should NOT be CA certificates
    cert_creation_data->ca = FALSE;
    cert_creation_data->crl_signing = FALSE;
    
    // Set certificate usage based on type
    if (current_wizard_type == WIZARD_CERT_TYPE_WEB_SERVER) {
        cert_creation_data->web_server = TRUE;
        cert_creation_data->web_client = FALSE;
        cert_creation_data->digital_signature = TRUE;
        cert_creation_data->key_encipherment = TRUE;
    } else {
        cert_creation_data->web_server = TRUE;
        cert_creation_data->web_client = TRUE;
        cert_creation_data->email_protection = TRUE;
        cert_creation_data->digital_signature = TRUE;
        cert_creation_data->key_encipherment = TRUE;
    }
    
    // Close dialog before async sign
    gtk_window_destroy(GTK_WINDOW(dialog));

    // Sign the CSR (async in GUI)
    new_cert_sign_csr (csr_id, ca_id, cert_creation_data,
                       __wizard_sign_csr_done_cb, NULL);
}

static void on_wizard_cancel_button_clicked (GtkButton *button, gpointer user_data)
{
    GtkWidget *dialog = GTK_WIDGET(user_data);
    gtk_window_destroy(GTK_WINDOW(dialog));
}

void wizard_window_display (WizardCertType cert_type)
{
    GtkWidget *dialog;
    GtkWidget *generate_button;
    GtkWidget *cancel_button;
    GtkWidget *cert_type_combo;
    GtkWidget *signing_ca_combo;
    GtkStringList *ca_string_list;
    GError *error = NULL;
    
    current_wizard_type = cert_type;
    
    // Load the UI file
    wizard_window_gtkb = gtk_builder_new ();
    if (!gtk_builder_add_from_file (wizard_window_gtkb,
                                     g_build_filename (PACKAGE_DATA_DIR, "gnomint", "wizard_window.ui", NULL),
                                     &error)) {
        g_warning ("Couldn't load builder file: %s", error ? error->message : "Unknown error");
        g_clear_error (&error);
        return;
    }
    
    // Get dialog widget
    dialog = GTK_WIDGET(gtk_builder_get_object (wizard_window_gtkb, "wizard_dialog"));
    
    if (!dialog) {
        g_warning ("Couldn't find wizard_dialog in UI file");
        g_object_unref (wizard_window_gtkb);
        return;
    }
    
    // Get widgets
    generate_button = GTK_WIDGET(gtk_builder_get_object (wizard_window_gtkb, "generate_button"));
    cancel_button = GTK_WIDGET(gtk_builder_get_object (wizard_window_gtkb, "cancel_button"));
    cert_type_combo = GTK_WIDGET(gtk_builder_get_object (wizard_window_gtkb, "cert_type_combo"));
    signing_ca_combo = GTK_WIDGET(gtk_builder_get_object (wizard_window_gtkb, "signing_ca_combo"));
    
    // Populate CA list using GtkStringList + parallel GArray of ca_ids
    GList *ca_list = __wizard_get_ca_list();

    if (!ca_list) {
        dialog_error (_("No Certificate Authority found. Please create a CA first."));
        g_object_unref (wizard_window_gtkb);
        return;
    }

    /* Create fresh GtkStringList and parallel ca_ids array */
    const gchar * const empty_strv[] = { NULL };
    ca_string_list = gtk_string_list_new (empty_strv);

    if (ca_ids) {
        g_array_unref (ca_ids);
    }
    ca_ids = g_array_new (FALSE, FALSE, sizeof (guint64));

    guint64 default_ca_id = __wizard_get_default_ca_id();
    guint default_index = 0;
    guint current_index = 0;
    time_t now = time(NULL);

    for (GList *l = ca_list; l != NULL; l = l->next) {
        CAInfo *ca = (CAInfo *)l->data;
        gchar *display_text;

        // Format display text with expiration date to distinguish CAs with same name
        if (ca->expiration > 0) {
            struct tm exp_tm;
            gnomint_gmtime(ca->expiration, &exp_tm);
            gchar exp_date[20];
            strftime(exp_date, sizeof(exp_date), "%Y-%m-%d", &exp_tm);

            if (ca->expiration < now) {
                display_text = g_strdup_printf("%s (exp: %s, expired)", ca->subject, exp_date);
            } else {
                display_text = g_strdup_printf("%s (exp: %s)", ca->subject, exp_date);
            }
        } else {
            display_text = g_strdup(ca->subject);
        }

        gtk_string_list_append (ca_string_list, display_text);
        g_array_append_val (ca_ids, ca->ca_id);

        g_free(display_text);

        if (ca->ca_id == default_ca_id) {
            default_index = current_index;
        }
        current_index++;

        g_free(ca->subject);
        g_free(ca);
    }

    g_list_free(ca_list);

    // Set the model on the signing CA dropdown and select default
    gtk_drop_down_set_model (GTK_DROP_DOWN(signing_ca_combo), G_LIST_MODEL(ca_string_list));
    g_object_unref (ca_string_list);
    gtk_drop_down_set_selected (GTK_DROP_DOWN(signing_ca_combo), default_index);

    // Set certificate type dropdown
    gtk_drop_down_set_selected (GTK_DROP_DOWN(cert_type_combo), cert_type);
    
    // Connect signals - builder will be unreferenced when dialog is destroyed
    g_object_add_weak_pointer (G_OBJECT(wizard_window_gtkb), (gpointer *)&wizard_window_gtkb);
    g_signal_connect_swapped (dialog, "destroy", G_CALLBACK(g_object_unref), wizard_window_gtkb);
    
    g_signal_connect (generate_button, "clicked", 
                      G_CALLBACK(on_wizard_generate_button_clicked), dialog);
    g_signal_connect (cancel_button, "clicked", 
                      G_CALLBACK(on_wizard_cancel_button_clicked), dialog);
    
    // Show dialog
    gtk_window_set_transient_for (GTK_WINDOW (dialog), dialog_get_main_window ());
    gtk_widget_set_visible(dialog, TRUE);
}
