package fr.monlouyan.bakefile;

import java.io.File;
import java.io.IOException;
import java.util.List;

/**
 * Exécuteur des commandes définies dans les règles.
 * Cette classe est responsable de l'exécution des commandes définies dans les règles
 * du fichier Bakefile.
 * 
 * @author Moncef STITI, Yanis HAMOUDI, Louay DARDOURI
 * @version 1.0
 */
public class CommandExecutor {
	/**
	 * true si le mode debug est activé, false sinon
	 */
    private boolean debug;

	/**
	 * Pour tracker si quelque chose doit être mis à jour
	 */
    private boolean needsUpdate = false;

	/**
	 * Pour tracker si un cycle a été détecté
	 */
    private boolean isCircular = false;
	
	/**
	 * Pour détecter les timestamps dans le futur
	 */
    private boolean futureTimestampDetected = false;

	/**
	 * Constructeur de la classe CommandExecutor.
	 * @param debug true si le mode debug est activé, false sinon
	 * @param isCircular true si on est en mode circulaire, false sinon
	 */
    public CommandExecutor(boolean debug, boolean isCircular) {
        this.debug = debug;
        this.isCircular = isCircular;
    }
    
	/**
	 * Exécute les commandes d'une règle.
	 * @param rule La règle à exécuter
	 */
    public void execute(Rule rule) {
        // On vérifie d'abord si cette règle a besoin d'être mise à jour
        boolean ruleNeedsUpdate = rule.needsUpdate();
        if (ruleNeedsUpdate) {
            needsUpdate = true;  // Au moins une règle doit être mise à jour
            
            // Vérifier les timestamps des dépendances pour détecter ceux dans le futur
            for (String dependency : rule.getDependencies()) {
                if (dependency.startsWith("~")) {
                    continue;
                }
                
                File depFile = new File(dependency);
                if (depFile.exists() && TimestampManager.getTimestamp(depFile) > System.currentTimeMillis()) {
                    futureTimestampDetected = true;
                }
            }
        }

        // Si la règle a besoin d'être mise à jour et qu'il n'y a pas de commandes
        if (ruleNeedsUpdate && rule.getCommands().isEmpty()) {
            return;  // On ne fait rien mais on ne montre pas de message
        }

        List<String> executableCommands = rule.getCommands();
        List<List<String>> displayCommands = rule.getDisplayCommands();

        for (int i = 0; i < executableCommands.size(); i++) {
            String command = executableCommands.get(i);
            List<String> displayLines = (i < displayCommands.size()) ? displayCommands.get(i) : null;
            
            // Vérifier si la commande commence par @ (ne pas afficher la commande)
            boolean silent = command.startsWith("@");
            // Enlever le @ si présent pour exécuter la commande correctement
            String actualCommand = silent ? command.substring(1) : command;
            
			boolean ignoreErrors = actualCommand.startsWith("-");

			if(ignoreErrors){
				actualCommand = actualCommand.substring(1).trim();
			}

            if (isCircular){
				if (!silent && displayLines != null && !displayLines.isEmpty()) {
					boolean isFirstLine = true;
					for (String line : displayLines) {
						if (isFirstLine) {
							// Pour la première ligne, supprimer l'indentation
							if (line.startsWith("\t")) {
								System.out.println(line.substring(1));
							} else {
								System.out.println(line);
							}
							isFirstLine = false;
						} else {
							// Pour les lignes suivantes, conserver l'indentation
							System.out.println(line);
						}
					}
				}
			}

            // On n'exécute que si nécessaire
            if (ruleNeedsUpdate) {
                try {
                    if(!isCircular && !silent){
						// Afficher les lignes formatées avec traitement spécial pour les continuations
						if (displayLines != null && !displayLines.isEmpty()) {
							boolean isFirstLine = true;
							
							for (String line : displayLines) {
								if (isFirstLine) {
									// Pour la première ligne, toujours supprimer l'indentation
									if (line.startsWith("\t")) {
										System.out.println(line.substring(1));
									} else {
										System.out.println(line);
									}
									isFirstLine = false;
								} else {
									// Pour les lignes suivantes d'une continuation, conserver l'indentation
									System.out.println(line);
								}
							}
						} else {
							// Cas d'une commande simple (une seule ligne)
							if (command.startsWith("\t")) {
								System.out.println(command.substring(1));
							} else {
								System.out.println(command);
							}
						}
					}
                    if (debug) System.out.println("Debug: Executing " + actualCommand);
                    ProcessBuilder pb = new ProcessBuilder("sh", "-c", actualCommand);
                    pb.inheritIO();
                    Process process = pb.start();

                    // Attendre la fin du processus
                    int exitCode = process.waitFor();

					if (exitCode != 0) {
						if (ignoreErrors) {
							System.err.println("bake: [" + rule.getName() + "] Error " + exitCode + " (ignored)");
						} else {
							System.err.println("bake: *** [" + rule.getName() + "] Error " + exitCode);
							System.exit(2);
						}
                    } else if (exitCode != 0 && ignoreErrors && debug) {
                        System.out.println("Debug: Command failed with exit code " + exitCode + ", but errors are ignored");
                    }

                } catch (IOException | InterruptedException e) {
                    if(!ignoreErrors){
						e.printStackTrace();
						System.exit(2);
					} else if(debug){
						System.out.println("Debug: Command execution failed, but errors are ignored");
						e.printStackTrace();
					}
                }
            }
        }

        // Si on a détecté des timestamps dans le futur, afficher un avertissement à la fin (comme make)
        if (futureTimestampDetected && !isCircular) {
            System.out.println("bake: warning:  Clock skew detected.  Your build may be incomplete.");
        }

        // Vérifier si cette règle est une cible directement demandée par l'utilisateur
        boolean isRequestedTarget = BakeCLI.getTargets().contains(rule.getName()) || 
                                   (BakeCLI.getTargets().isEmpty() && rule.getName().equals(BakefileParser.getFirstTarget()));
        
        if (!isRequestedTarget || needsUpdate) {
            // Si ce n'est pas une cible demandée ou si une mise à jour a été nécessaire,
            // ne pas afficher de message
            return;
        }

        // Vérifier si la cible existe en tant que fichier
        File targetFile = new File(rule.getName());
        boolean fileExists = targetFile.exists();
        
        if (!rule.getCommands().isEmpty() && fileExists) {
            // Si la cible a des commandes et existe en tant que fichier, 
            // afficher "up to date"
            System.out.println("bake: `" + rule.getName() + "' is up to date.");
        } else {
            // Si la cible n'a pas de commandes ou n'existe pas en tant que fichier,
            // afficher "Nothing to be done"
            System.out.println("bake: Nothing to be done for `" + rule.getName() + "'.");
        }
    }
}