package com.ejie.ad74a;

import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogManager;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.context.support.GenericApplicationContext;

import com.ejie.ad74a.context.bd.DataBaseInfo;
import com.ejie.ad74a.exception.BatchProcessException;
import com.ejie.ad74a.process.IProcess;
import com.ejie.ad74a.util.UtilDates;
import com.ejie.ad74a.util.UtilFiles;
import com.ejie.ad74a.util.UtilProperties;

/**
 * Lanzador generico de procesos batch.
 * 
 * @author JBHG
 *
 */
public class BatchLauncher {

    private static Logger logger = LoggerFactory.getLogger(BatchLauncher.class);
    private static ApplicationContext context = null;
    // Nombre del bean utilizado para guardar los datos de conexion a bbdd
    private static final String DB_BEAN_NAME = "dataBaseInfo";
    // Ruta al xml de configuracion de spring.
    private static final String SPRING_XML_CONF_PATH = "/ad74a/spring-context.xml";

    private static FileChannel fchannel = null;
    private static FileLock fileLock = null;

    private static final String FORCE_ARGUMENT = "FORCE=TRUE";

    private BatchLauncher() {
    }

    /**
     * Lanzador de procesos batch generico. Debe recibir 4 parametros.
     * <ul>
     * <li>BD (de la cadena de conexion a BBDD)</li>
     * <li>Usuario (de la cadena de conexion a BBDD)</li>
     * <li>Password (de la cadena de conexion a BBDD)</li>
     * <li>Clase del proceso que se quiere ejecutar (debe implementar la interfaz
     * {@link IProcess})</li>
     * </ul>
     * <p>
     * Los tres primeros parametros son los que devuelve la llamada a la cadena
     * <b><code>"/aplic/n38/cade/n38bd"</code></b>.
     * </p>
     * <p>
     * Si se indica el parametro "<b>force=true</b>" ({@link FORCE_ARGUMENT}) el proceso se
     * ejecutará aunque no se haya podido obtener el bloqueo de ser el unico proceso ejecutándose.
     * Este parámetro no se pasará a la lista de parámetros recibidos por el proceso a ejecutar.
     * </p>
     * 
     * @param args
     *            bd, user, password, className [, argumentos para el proceso][force=true]
     */
    public static void main(String[] arguments) {
        String[] args = argsWithoutForceTrue(arguments);
        if (args == null || args.length < 4) {
            throw new IllegalArgumentException(
                    "Deben informarse al menos cuatro argumentos: BD, USUARIO, PASSWORD y Clase a ejecutar");
        }
        String processName = args[3];
        int resultado = 0;
        try {
            setThreadName(processName);
            // Se crea contexto de spring
            long milliLog = System.currentTimeMillis();
            createContext(args);
            logger.info("SE INICIA EL PROCESO: {}", processName);
            if (logger.isDebugEnabled()) {
                milliLog = System.currentTimeMillis() - milliLog;
                logger.debug("Se ha inicializado contexto de Spring en {} segundos", milliLog / 1000.0);
            }
            if (args.length < arguments.length) {
                logger.info(
                        "PROCESO '{}' forzado a ejecutarse. No se comprueba si ya hay otra instancia de este proceso ejecutandose.",
                        processName);
            } else if (!tryLock(processName)) {
                logger.warn("PROCESO '{}' BLOQUEADO. Ya hay otra instancia de este proceso ejecutandose.", processName);
                return;
            }
            // Preparacion de los argumentos que va a recibir el proceso
            String[] argsExecute = new String[args.length - 4];
            int x = 0;
            for (int i = 4; i < args.length; i++, x++) {
                argsExecute[x] = args[i];
            }
            IProcess processBean = context.getBean(processName, IProcess.class);
            milliLog = System.currentTimeMillis();
            if(logger.isDebugEnabled()) {
                logger.debug("Se lanza el proceso '{}' con los argumentos: {}", processName,
                        Arrays.toString(argsExecute));
            }
            // ****************************** //
            // **** SE LANZA EL PROCESO: **** //
            // ****************************** //
            processBean.execute(argsExecute);
            // ****************************** //
            milliLog = System.currentTimeMillis() - milliLog;
            long[] timesLog = UtilDates.getHhMmSs(milliLog);
            logger.debug("El proceso '{}' ha tardado: {}hh {}mm {}ss", processName, timesLog[0], timesLog[1],
                    timesLog[2]);
        } catch (BatchProcessException e) {
            logger.error("Error en el proceso batch '" + processName + "'.", e);
            resultado = 1;
        } catch (Throwable e) {//NOSONAR Necesario tracear excepciones no controladas
            logger.error("Se ha producido una excepcion en el proceso batch '" + processName + "'.", e);
            resultado = 2;
        } finally {
            logger.info("FINALIZA EL PROCESO: {}", processName);
            // Se cierra contexto de spring
            try {
                closeContext();
            } catch (Throwable e) {// NOSONAR Necesario para estar seguros de que se libera el bloqueo del proceso
                logger.warn("Error al cerrar el contexto. Se continua con la finalizacion del proceso", e);
            }
            // Se libera el bloqueo para este tipo de procesos
            try {
                releaseLock(processName);
            } catch (BatchProcessException e) {
                logger.error("Se ha producido una excepcion al liberar el bloqueo del proceso '" + processName + "'.",
                        e);
                resultado = 3;
            }
            System.exit(resultado);
        }

    }

    /*
     * Crea el contexto para lanzar procesos batch
     */
    private static void createContext(String[] args) {
        /*
         * Para no mostrar trazas de inicializacion de Spring. Solo si ocurren errores.
         */
        // Logback:
        ((ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME))
                .setLevel(ch.qos.logback.classic.Level.WARN);
        // Apache commons loggin:
        java.util.logging.Logger rootLogger = LogManager.getLogManager().getLogger("");
        rootLogger.setLevel(Level.WARNING);
        for (Handler h : rootLogger.getHandlers()) {
            h.setLevel(Level.WARNING);
        }

        // Se recogen los argumentos de la cadena de conexion, necesarios
        // para construir un objeto DataBaseInfo
        String[] bbddArgs = { args[0], args[1], args[2] };
        // se inicia el contexto de Spring
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.rootBeanDefinition(DataBaseInfo.class);
        for (String argConst : bbddArgs) {
            beanDefinitionBuilder.addConstructorArgValue(argConst);
        }
        // 1- se registra el bean para la cadena de conexion a BBDD
        beanFactory.registerBeanDefinition(DB_BEAN_NAME, beanDefinitionBuilder.getBeanDefinition());
        // 2- se crea un contexto generico solo para dicho bean
        GenericApplicationContext genericApplicationContext = new GenericApplicationContext(beanFactory);
        genericApplicationContext.refresh();
        // 3- se crea el contexto de app, partiendo del contexto generico y de
        // un xml de definicion
        
       
        context = new ClassPathXmlApplicationContext(new String[] { SPRING_XML_CONF_PATH }, genericApplicationContext);
    }

    /*
     * Cierra el contexto creado
     */
    private static void closeContext() {
        ((GenericApplicationContext)context.getParent()).close();
    }

    /**
     * Obtiene un bean del contexto de spring del proceso
     * 
     * @param clazz
     *            Clase del bean que se quiere recuperar
     * 
     * @return ApplicationContext
     */
    public static final <T> T getBean(Class<T> clazz) {
        return context.getBean(clazz);
    }

    /*
     * Se modifica el nombre del thread para que aporte informacion a los logs sobre el proceso que
     * se esta ejecutando.
     */
    private static void setThreadName(String process) {
        Thread t = Thread.currentThread();
        t.setName(process);
    }

    /*
     * Devuelve el array de argumentos sin el parametro 'force=true'
     */
    private static String[] argsWithoutForceTrue(String[] args) {
        List<String> listArgs = new ArrayList<>();
        for (String i : args) {
            if (!FORCE_ARGUMENT.equalsIgnoreCase(i)) {
                listArgs.add(i);
            }
        }
        return listArgs.toArray(new String[0]);
    }

    /*
     * Intenta obtener el bloqueo de ejecución. Devuelve True si lo consigue.
     */
    private static boolean tryLock(String processName) throws BatchProcessException {
        try {
            Path p = Paths.get(UtilFiles.normalizeFolder(UtilProperties.Props.PATH_TMP.val()));
            Files.createDirectories(p);
            p = p.resolve(processName + ".lock");
            if (!p.toFile().exists()) {
                Files.createFile(p);
            }
            fchannel = FileChannel.open(p, StandardOpenOption.WRITE);
            fileLock = fchannel.tryLock();
            return fileLock != null;
        } catch (IOException e) {
            closeLockChannel();
            throw new BatchProcessException(
                    "Error al intentar generar el fichero de bloqueo para el proceso " + processName + ".", e);
        }
    }

    /*
     * Intenta liberar el bloqueo de ejecución.
     */
    private static void releaseLock(String processName) throws BatchProcessException {
        try {
            if (fileLock != null) {
                fileLock.release();
                Path p = Paths
                        .get(UtilFiles.normalizeFolder(UtilProperties.Props.PATH_TMP.val()) + processName + ".lock");
                Files.deleteIfExists(p);
            }
        } catch (IOException e) {
            throw new BatchProcessException(e);
        } finally {
            closeLockChannel();
        }
    }

    /*
     * Cierra el canal utilizado para el archivo de bloqueo
     */
    private static void closeLockChannel() {
        if (fchannel != null) {
            try {
                fchannel.close();
            } catch (IOException e) {
                logger.warn("Se ha producido un error al cerrar el canal del archivo de bloqueo", e);
            }
        }
    }
}
