package com.fitbank.bpm.client;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.sql.Blob;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.hibernate.HibernateException;
import org.hibernate.SQLQuery;
import org.hibernate.SessionFactory;
import org.hibernate.Session;
import org.hibernate.exception.JDBCConnectionException;
import org.jbpm.api.Execution;
import org.jbpm.api.ExecutionService;
import org.jbpm.api.HistoryService;
import org.jbpm.api.ManagementService;
import org.jbpm.api.ProcessDefinition;
import org.jbpm.api.ProcessDefinitionQuery;
import org.jbpm.api.ProcessEngine;
import org.jbpm.api.ProcessInstance;
import org.jbpm.api.RepositoryService;
import org.jbpm.api.TaskService;
import org.jbpm.api.model.ActivityCoordinates;

import com.fitbank.common.exception.FitbankException;
import com.fitbank.common.logger.FitbankLogger;
import com.fitbank.dto.management.Detail;
import com.fitbank.dto.management.Field;

/**
 * Clase que maneja flujos. Tiene métodos para inicializar, consultar y enviar
 * señales a procesos.
 *
 * @author BANTEC Inc.
 */
public class BPMProcessor {

    private static final Logger LOGGER = FitbankLogger.getLogger();

    private static final Configuration CONFIG = BPMProperties.getConfig();

    private static ProcessEngine processEngine;

    private static RepositoryService repositoryService;

    private static ExecutionService executionService;

    private static TaskService taskService;

    private static HistoryService historyService;

    private static ManagementService managementService;

    private final ProcessInstance processInstance;

    private final List<String> pids = new ArrayList<String>();

    static {
        init();
    }

    private static void init() {
        File file = new File(CONFIG.getString("fit.bpm.cfg", ""));

        if (file.exists()) {
            org.jbpm.api.Configuration cfg = new org.jbpm.api.Configuration();
            cfg.setFile(file);
            processEngine = cfg.buildProcessEngine();
        } else {
            processEngine = org.jbpm.api.Configuration.getProcessEngine();
        }

        BPMProcessor.repositoryService = processEngine.getRepositoryService();
        BPMProcessor.executionService = processEngine.getExecutionService();
        BPMProcessor.taskService = processEngine.getTaskService();
        BPMProcessor.historyService = processEngine.getHistoryService();
        BPMProcessor.managementService = processEngine.getManagementService();
    }

    /**
     * Busca un proceso por id.
     *
     * @param id ID del proceso
     *
     * @return un BPMProcessor para manejar el proceso o null si no se encontró la
     * instancia
     */
    public static BPMProcessor findProcessInstanceById(String id) {
        ProcessInstance processInstance = findProcessInstanceById_(id);

        if (processInstance == null) {
            return null;
        } else {
            return new BPMProcessor(processInstance);
        }
    }

    /**
     * Inicia un proceso.
     *
     * @param processDefinitionKey Nombre del proceso.
     * @param variables Variables que se pasarán al proceso.
     *
     * @return un BPMProcessor para manejar el proceso
     */
    public static BPMProcessor startProcessInstanceByKey(
            String processDefinitionKey, Map<String, Object> variables) {
        return new BPMProcessor(startProcessInstanceByKey_(processDefinitionKey,
                variables));
    }

    private static ProcessInstance findProcessInstanceById_(String id) {
        try {
            return BPMProcessor.executionService.findProcessInstanceById(id);
        } catch (JDBCConnectionException ex) {
            // FIXME
            LOGGER.error("No se pudo conectar, reintentando???", ex);
            init();
            return BPMProcessor.executionService.findProcessInstanceById(id);
        }
    }

    private static ProcessInstance startProcessInstanceByKey_(
            String processInstanceId, Map<String, Object> variables) {
        try {
            return BPMProcessor.executionService.startProcessInstanceByKey(
                    processInstanceId, variables);
        } catch (JDBCConnectionException ex) {
            // FIXME
            LOGGER.error("No se pudo conectar, reintentando???", ex);
            init();
            return BPMProcessor.executionService.startProcessInstanceByKey(
                    processInstanceId, variables);
        }
    }

    /**
     * Construye un BPMProcessor con la instancia del flujo indicada.
     *
     * @param processInstance Instancia del flujo para este objeto.
     */
    public BPMProcessor(ProcessInstance processInstance) {
        this.processInstance = processInstance;
    }

    public ProcessInstance getProcessInstance() {
        return this.processInstance;
    }

    public String getPid() {
        return this.processInstance.getId();
    }

    public Set<String> getVariableNames() {
        return executionService.getVariableNames(getPid());
    }

    public Object getVariable(String name) {
        return executionService.getVariable(getPid(), name);
    }

    public void setVariable(String name, Object object) {
        executionService.setVariable(getPid(), name, object);
    }

    public List<String> getPids() {
        return Collections.unmodifiableList(this.pids);
    }

    /**
     * Obtiene una lista de nombres de actividades del proceso actual.
     *
     * @return Lista
     */
    public List<String> findActualStates() {
        Set<String> names = this.processInstance.findActiveActivityNames();
        return new LinkedList<String>(names);
    }

    /**
     * Crea una imagen para el flujo actual.
     *
     * @return Un string codificado en base64 de la imagen.
     */
    public String createImageBase64() {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        createImage(baos);
        return Base64.encodeBase64String(baos.toByteArray());
    }

    /**
     * Crea una imagen para el flujo actual y la escribe en el OutputStream.
     *
     * @param out Objeto donde se escribirá la imagen, no se cerrará
     */
    public void createImage(OutputStream out) {
        Session session = openSession();

        RepositoryService rs = repositoryService;
        ProcessDefinitionQuery pdq = rs.createProcessDefinitionQuery();
        String id = getProcessInstance().getProcessDefinitionId();
        ProcessDefinition pd = pdq.processDefinitionId(id).uniqueResult();

        LOGGER.debug("Consultando imagen para el flujo " + pd.getDeploymentId()
                + " " + pd.getImageResourceName());

        SQLQuery query = session.createSQLQuery("SELECT BLOB_VALUE_"
                + " FROM JBPM4_LOB"
                + " WHERE DEPLOYMENT_=?"
                + "   AND NAME_ LIKE ?");

        query.setString(0, pd.getDeploymentId());
        query.setString(1, pd.getImageResourceName());

        Blob img = (Blob) query.uniqueResult();
        if (img != null) {
            try {
                InputStream in = img.getBinaryStream();
                IOUtils.copy(in, out);
                in.close();
            } catch (IOException ex) {
                throw new FitbankException(ex);
            } catch (SQLException ex) {
                throw new FitbankException(ex);
            }
        }

        session.close();
    }

    /**
     * Encuentra el id de la ejecución dado un nombre de actividad.
     *
     * @param activityName Nombre de la actividad.
     *
     * @return El id de la ejecución.
     */
    public String findExecutionId(String activityName) {
        // TODO revisar si realmente no es lo mismo que: return processInstance.findActiveExecutionIn(activityName).getKey();
        Session session = openSession();

        SQLQuery query = session.createSQLQuery("SELECT b.ID_"
                + " FROM JBPM4_EXECUTION a, JBPM4_EXECUTION b"
                + " WHERE a.ID_=?"
                + "   AND a.SUBPROCINST_ IS NOT NULL"
                + "   AND b.INSTANCE_=a.SUBPROCINST_ "
                + "   AND b.PARENT_IDX_ IS NULL"
                + "   AND a.ACTIVITYNAME_=?");

        query.setString(0, getPid());
        query.setString(1, activityName);

        Object res = query.uniqueResult();

        session.close();

        return res == null ? null : String.valueOf(res);
    }

    /**
     * Obtiene un objeto ActivityMetaData para una actividad.
     *
     * @param activityName Nombre de la actividad
     *
     * @return objeto ActivityMetaData
     */
    public ActivityMetaData findStatesMetaData(String activityName) {
        String processDefinitionId =
                this.processInstance.getProcessDefinitionId();
        ActivityCoordinates coo = BPMProcessor.repositoryService.
                getActivityCoordinates(processDefinitionId, activityName);
        try {
            String subPID = this.findExecutionId(activityName);
            this.pids.add(subPID);
            LOGGER.info("Actividad " + activityName + ">>" + subPID);
            return new ActivityMetaData(activityName, subPID, coo.getX(),
                    coo.getY(), coo.getHeight(), coo.getWidth());

        } catch (Exception e) {
            // FIXME entender esto
            LOGGER.warn("No se pudo obtener por pid???", e);
            return new ActivityMetaData(activityName, coo.getX(), coo.getY(),
                    coo.getHeight(), coo.getWidth());
        }
    }

    public Map<String, Object> getSubVariablesById(String pState) {
        Map<String, Object> m = new HashMap<String, Object>();
        String subId = this.findExecutionId(pState);
        if (subId != null) {
            BPMProcessor cli = BPMProcessor.findProcessInstanceById(subId);
            m = cli.getSubVariablesActualFirst();
            if (!m.isEmpty()) {
                return m;
            }
            for (String var : cli.getVariableNames()) {
                m.put(var, cli.getVariable(var));
            }
        }
        return m;
    }

    public Map<String, Object> getSubVariablesActualFirst() {
        try {
            String state = this.findActualStates().iterator().next();
            return this.getSubVariablesById(state);
        } catch (Exception e) {
            LOGGER.warn(e, e);
            return new HashMap<String, Object>();
        }

    }

    public boolean getAuthorizer(String pState, String user) {
        try {
            Map<String, Object> m;
            if (pState == null) {
                m = this.getSubVariablesActualFirst();
            } else {
                m = this.getSubVariablesById(pState);
            }
            LOGGER.debug(m);
            if (m != null) {
                Detail det = (Detail) m.get("detail");
                if (det != null) {
                    List<String> users = this.getUsersNotify(det);
                    LOGGER.debug(users);
                    for (String u : users) {
                        if (u.equals(user)) {
                            return true;
                        }
                    }
                }
            }
        } catch (Exception e) {
            LOGGER.error("Excepción desconocida", e);
        }

        return false;
    }

    private List<String> getUsersNotify(Detail pDetail) {
        List<String> users = new ArrayList<String>();
        for (Field f : pDetail.getFields()) {
            if (f.getName().indexOf("_USER_NOTIFY") == 0) {
                users.add(f.getStringValue());
            }
        }
        return users;
    }

    public Map<String, Map<String, Object>> getSubVariables() {
        Map<String, Map<String, Object>> data =
                new HashMap<String, Map<String, Object>>();
        List<String> status = this.findActualStates();
        for (String state : status) {
            data.put(state, this.getSubVariablesById(state));
        }
        return data;
    }

    public List<ActivityMetaData> getActualMetadata() {
        List<String> status = this.findActualStates();
        List<ActivityMetaData> metadata = new ArrayList<ActivityMetaData>();
        for (String state : status) {
            metadata.add(this.findStatesMetaData(state));
        }
        return metadata;
    }

    public void sendNO() {
        this.sendSign("no");
    }

    public void sendNO(String pActivity) {
        this.sendSign(pActivity, "no");
    }

    public void sendOK() {
        this.sendSign("ok");
    }

    public void sendOK(String pActivity) {
        this.sendSign(pActivity, "ok");
    }

    public void sendSign(String pSignal) {
        if (StringUtils.isEmpty(pSignal)) {
            this.sendSign();
        } else {
            BPMProcessor.executionService.signalExecutionById(this.processInstance.
                    getId(), pSignal);
        }
    }

    public void sendSign() {
        BPMProcessor.executionService.signalExecutionById(this.processInstance.
                getId());
    }

    public void sendSignSub() {
        this.sendSignSub("");
    }

    public void sendSignSub(String pSignal) {
        LOGGER.info("PID:" + getPid() + " SIGN:" + pSignal);
        List<String> states = this.findActualStates();
        String state = states.iterator().next();
        this.sendSign(state, pSignal);
    }

    public void sendSign(String pActivity, String pSignal) {
        String subId = this.findExecutionId(pActivity);

        if (subId != null) {
            BPMProcessor cli = BPMProcessor.findProcessInstanceById(subId);
            List<String> states = cli.findActualStates();
            String state = states.iterator().next();
            cli.sendSign(state, pSignal);
        } else {
            this.sendSign(pSignal);
        }
    }

    public void end() {
        this.end(Execution.STATE_ENDED);
    }

    public void end(String pState) {
    	BPMProcessor.executionService.endProcessInstance(getPid(), pState);
    	List<String> res = this.getEndedInstances();
    	if (res != null) {
    		Iterator instanciasFinalizadas = res.iterator();
    		while (instanciasFinalizadas.hasNext()) {
	            BPMProcessor cli = BPMProcessor.findProcessInstanceById(String.valueOf( instanciasFinalizadas.next()));
	            BPMProcessor.executionService.deleteProcessInstance(cli.getPid());
    		}
    	}
    }
    private List  getEndedInstances () {
    	Session session = openSession();

        SQLQuery query = session.createSQLQuery("select ID_ from JBPM4_EXECUTION " +
        					 "where STATE_ = ? ");
     
        query.setString(0, Execution.STATE_ENDED);
        Object res = query.list();
        session.close();

        return res == null ? null : (List) res;
    	
    }
    private Session openSession() throws HibernateException {
        return processEngine.get(SessionFactory.class).openSession();
    }

    //<editor-fold defaultstate="collapsed" desc="Otros">
    public ExecutionService getExecutionService() {
        return BPMProcessor.executionService;
    }

    public HistoryService getHistoryService() {
        return BPMProcessor.historyService;
    }

    public ManagementService getManagementService() {
        return BPMProcessor.managementService;
    }

    public ProcessEngine getProcessEngine() {
        return BPMProcessor.processEngine;
    }

    public RepositoryService getRepositoryService() {
        return BPMProcessor.repositoryService;
    }

    public TaskService getTaskService() {
        return BPMProcessor.taskService;
    }
    //</editor-fold>

    //<editor-fold defaultstate="collapsed" desc="Métodos deprecados">
    /**
     * @deprecated Mejor Usar los métodos estáticos de las clases.
     */
    @Deprecated
    public BPMProcessor() {
        this.processInstance = null;
    }

    /**
     * @deprecated Mejor Usar findProcessInstanceById
     */
    @Deprecated
    public BPMProcessor(String id) {
        this(findProcessInstanceById_(id));
        if (this.processInstance == null) {
            throw new FitbankException("BPM097",
                    "EL FLUJO {0} YA HA SIDO FINALIZADO", id);
        }
    }

    /**
     * @deprecated Mejor Usar startProcessInstanceByKey
     */
    @Deprecated
    public BPMProcessor(String processInstanceId, Map<String, Object> variables) {
        this(startProcessInstanceByKey_(processInstanceId, variables));
    }
    //</editor-fold>

}
