#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <time.h>
#include <sys/wait.h>
#include <string.h>

#define ACCOUNT_SIZE 16
#define MAX_NAME_LEN 10
#define FILENAME "../Data/accounts.db"

// Couleurs pour l'affichage
#define RESET   "\033[0m"
#define GREEN   "\033[32m"
#define RED     "\033[31m"
#define PINK  "\033[35m"
#define BLUE    "\033[34m"

/**
 * @brief Écrit les informations d'un compte dans un fichier sans verrouillage.
 * 
 * Cette fonction écrit un compte spécifié par son index dans le fichier
 * spécifié par le descripteur de fichier. Avant d'écrire, elle formate les
 * données du compte (nom et solde) pour les rendre compatibles avec la taille
 * d'un enregistrement.
 *
 * @param fd Le descripteur de fichier du fichier contenant les comptes.
 * @param index L'index du compte à écrire (en termes de position dans le fichier).
 * @param name Le nom du compte à écrire.
 * @param balance Le solde du compte à écrire.
 * 
 */
void read_account(int fd, int index, char *name, int *balance) {
    char buffer[ACCOUNT_SIZE];

    // Se positionner à l'index du compte dans le fichier
    lseek(fd, index * ACCOUNT_SIZE, SEEK_SET);

    // Lire l'enregistrement du compte
    read(fd, buffer, ACCOUNT_SIZE);

    // Extraire le nom et le solde du compte
    sscanf(buffer, "%7s %d", name, balance);
}


/**
 * @brief Écrit les informations d'un compte dans un fichier sans verrouillage.
 * 
 * Cette fonction écrit un compte spécifié par son index dans le fichier
 * spécifié par le descripteur de fichier. Avant d'écrire, elle formate les
 * données du compte (nom et solde) pour les rendre compatibles avec la taille
 * d'un enregistrement.
 *
 * @param fd Le descripteur de fichier du fichier contenant les comptes.
 * @param index L'index du compte à écrire (en termes de position dans le fichier).
 * @param name Le nom du compte à écrire.
 * @param balance Le solde du compte à écrire.
 * 
 */
void write_account(int fd, int index, const char *name, int balance) {
    char buffer[ACCOUNT_SIZE];

    // Formater la chaîne avec un nom et un solde
    snprintf(buffer, ACCOUNT_SIZE, "%-7s%8d", name, balance);

    // S'assurer que le buffer est exactement de taille ACCOUNT_SIZE
    for (int i = strlen(buffer); i < ACCOUNT_SIZE - 1; i++) {
        buffer[i] = ' ';
    }
    buffer[ACCOUNT_SIZE - 1] = '\n';  // S'assurer que la ligne est terminée correctement

    lseek(fd, index * ACCOUNT_SIZE, SEEK_SET);
    write(fd, buffer, ACCOUNT_SIZE);
}

/**
 * @brief Effectue une transaction aléatoire entre deux comptes sans verrouillage.
 * 
 * Cette fonction simule une transaction entre deux comptes distincts sélectionnés
 * aléatoirement. Le montant transféré est calculé aléatoirement et ne dépasse pas
 * 20% du solde du compte source.
 * 
 * @param fd Le descripteur de fichier du fichier contenant les comptes.
 * @param account_count Le nombre total de comptes dans le fichier.
 * 
 * @note Les comptes source et destination sont choisis de manière aléatoire, 
 * mais un compte ne peut pas transférer à lui-même.
 * @note La transaction est annulée si le solde du compte source est insuffisant
 * pour effectuer un transfert minimal.
 * 
 */
void perform_transaction(int fd, int account_count) {
    int from_idx = rand() % account_count;
    int to_idx = rand() % account_count;

    // Sélectionner un autre compte si source et destination sont identiques
    while (from_idx == to_idx) {
        to_idx = rand() % account_count;
    }

    char from_name[MAX_NAME_LEN], to_name[MAX_NAME_LEN];
    int from_balance, to_balance;

    // Lire les comptes source et destination
    read_account(fd, from_idx, from_name, &from_balance);
    read_account(fd, to_idx, to_name, &to_balance);

    // Calculer un montant aléatoire à transférer (max 20% du solde source)
    int max_transfer = from_balance / 5;
    if (max_transfer > 0) {
        int transfer_amount = rand() % max_transfer + 1;

        // Effectuer la transaction
        from_balance -= transfer_amount;
        to_balance += transfer_amount;

        // Écrire les nouveaux soldes dans le fichier
        write_account(fd, from_idx, from_name, from_balance);
        write_account(fd, to_idx, to_name, to_balance);

        printf(GREEN "Transaction : %s -> %s : %d€\n" RESET, from_name, to_name, transfer_amount);
    } else {
        printf(RED "Transaction annulée : Fonds insufisants %s\n" RESET, from_name);
    }
}

/**
 * @brief Effectue un ensemble de transactions dans un processus.
 * 
 * Cette fonction ouvre le fichier contenant les comptes en mode lecture/écriture
 * et effectue un nombre donné de transactions aléatoires. Chaque transaction est
 * réalisée en appelant la fonction `perform_transaction`. Une temporisation aléatoire
 * est introduite entre les transactions pour simuler un comportement réaliste.
 * 
 * @param transactions Le nombre de transactions à effectuer.
 * @param account_count Le nombre total de comptes dans le fichier.
 * 
 * @note La fonction ouvre le fichier spécifié par `FILENAME` en mode lecture/écriture.
 * Elle s'assure de le fermer correctement à la fin.
 * 
 * @warning Si l'ouverture du fichier échoue, la fonction interrompt le programme
 * avec un appel à `exit(1)`.
 * 
 * @details
 * - La temporisation introduite par `usleep` (avec une durée aléatoire) contribue à la simulation d'un trafic réseau ou de retards dans les transactions.
 */
void process_transactions(int transactions, int account_count) {
    int fd = open(FILENAME, O_RDWR);
    if (fd < 0) {
        perror("Erreur : Impossible d'ouvrir le fichier");
        exit(1);
    }

    for (int i = 0; i < transactions; i++) {
        perform_transaction(fd, account_count);
        usleep(rand() % 100000); // Temporisation aléatoire
    }

    close(fd);
}

/**
 * @brief Calcule le solde total de tous les comptes dans le fichier.
 * 
 * Cette fonction parcourt tous les comptes stockés dans le fichier spécifié
 * par `FILENAME`, en lisant leurs soldes et en les additionnant pour produire
 * un solde total. Elle ouvre le fichier en mode lecture seule, le parcourt
 * compte par compte, puis le ferme avant de retourner le résultat.
 * 
 * @return Le solde total de tous les comptes, ou -1 en cas d'erreur
 * (par exemple, si l'ouverture du fichier échoue).
 * 
 * @note La taille de chaque compte est définie par la constante `ACCOUNT_SIZE`.
 * 
 * @warning Cette fonction ne valide pas les données des comptes lues depuis le fichier.
 * Elle suppose que les données sont correctement formatées.
 * 
 */
int calculate_total_balance() {
    int fd = open(FILENAME, O_RDONLY);
    if (fd < 0) {
        perror("Erreur : Impossible d'ouvrir le fichier");
        return -1;
    }

    off_t file_size = lseek(fd, 0, SEEK_END);
    int account_count = file_size / ACCOUNT_SIZE;

    int total_balance = 0;
    char name[MAX_NAME_LEN];
    int balance;

    for (int i = 0; i < account_count; i++) {
        read_account(fd, i, name, &balance);
        total_balance += balance;
    }

    close(fd);
    return total_balance;
}

// Fonction principale
int main(int argc, char *argv[]) {
    // Vérifier que deux arguments sont passés en paramètre
    if (argc != 3) {
        fprintf(stderr, "Usage: %s <number_of_processes> <transactions_per_process>\n", argv[0]);
        return 1;
    }

     // Extraire le nombre de processus et le nombre de transactions par processus
    int num_processes = atoi(argv[1]);
    int transactions_per_process = atoi(argv[2]);

    // Initialiser une graine aléatoire basée sur l'ID de processus et l'heure actuelle
    // On aurait aussi pu utiliser la fonction rand() directement, mais rand_r() est plus sûr
    // Car il permet d'éviter les interférences entre les processus
    unsigned int seed = getpid() ^ time(NULL);
    int random_value = rand_r(&seed);

    // Calcul du solde total initial
    int initial_balance = calculate_total_balance();
    if (initial_balance < 0) {
        return 1; // Erreur lors de l'ouverture du fichier
    }
    printf("Solde total initial : %d€\n", initial_balance);

    // Ouvrir le fichier pour déterminer le nombre de comptes
    int fd = open(FILENAME, O_RDONLY);
    if (fd < 0) {
        perror("Erreur : Impossible d'ouvrir le fichier");
        return 1;
    }
    off_t file_size = lseek(fd, 0, SEEK_END);
    close(fd);

    int account_count = file_size / ACCOUNT_SIZE;

    printf("Nombre de comptes : %d\n", account_count);

    // Créer les processus enfants
    for (int i = 0; i < num_processes; i++) {
        if (fork() == 0) {
            process_transactions(transactions_per_process, account_count);
            exit(0);
        }
    }

    // Attendre les processus enfants
    for (int i = 0; i < num_processes; i++) {
        wait(NULL);
    }

    // Calcul du solde total final
    int final_balance = calculate_total_balance();
    if (final_balance < 0) {
        return 1; // Erreur lors de l'ouverture du fichier
    }
    printf("Solde total final : %d€\n", final_balance);

    // Vérifier que le solde initial et le solde final sont identiques
    if (initial_balance != final_balance) {
        printf("Le solde final est différent du solde initial...\n");
    }

    printf("Tout les processus sont terminés !\n");

    return 0;
}