import com.jscape.inet.mft.application.*;
import com.jscape.inet.mft.events.DomainEvent;
import com.jscape.util.ConfigurationProperties;
import com.jscape.util.Time;
import com.jscape.util.cli.Console;
import com.jscape.util.cli.ext.CommandLineService;
import com.jscape.util.cli.ext.InvalidArgumentsException;
import com.jscape.util.cli.ext.PairOption;
import com.jscape.util.cli.ext.SingleOption;
import com.jscape.util.service.AbstractService;
import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.pool.PooledConnectionFactory;

import javax.jms.ConnectionFactory;
import javax.jms.Message;
import javax.jms.ObjectMessage;

import static com.jscape.util.Parameters.assertGreater;
import static com.jscape.util.Parameters.assertNotNull;
import static com.jscape.util.log.LogTools.suppressLogging;

/**
 * JMS event processing application.
 * An example of remote JSCAPE MFT Server event processing.
 *
 * @author Alex Usun
 */
public class JmsSyncEventProcessingApplication
        extends AbstractService<Void>
        implements JmsConsumerService.Listener {

    private static final Time JMS_RECONNECT_ATTEMPT_INTERVAL = Time.seconds(5);

    private final Console console;
    private final JmsConsumerService jmsConsumerService;
    private final MftSyncEventProcessingService eventProcessingService;
    private final int maxProcessingAttempts;

    public static void main(String[] arguments)
            throws Exception {
        suppressLogging();

        Console console = Console.systemConsole();
        console.println("JMS Sync Event Processing Application v. 1.0");

        try {
            if (Configuration.helpRequested(arguments)) {
                showUsage(console);
            } else {
                Configuration configuration = Configuration.configurationFor(arguments);

                JmsSyncEventProcessingApplication application = applicationFor(configuration, console);
                application.start();

                awaitExit(console);
                application.dispose();
            }

            System.exit(0);
        } catch (InvalidArgumentsException e) {
            showConfigurationError(console, e);
            showUsage(console);
            System.exit(1);
        } catch (ConfigurationProperties.UnsupportedValueException e) {
            showConfigurationError(console, e);
            System.exit(1);
        } catch (Throwable e) {
            showError(console, e);
            System.exit(1);
        }
    }

    private static void showUsage(Console console) {
        console.printf(Configuration.USAGE);
    }

    private static void showConfigurationError(Console console, Exception e) {
        console.printf("Configuration error. %s%n", e.getMessage() != null ? e.getMessage() : "Unsupported configuration/command line arguments.");
    }

    private static void showError(Console console, Throwable error) {
        console.print(error);
    }

    private static void awaitExit(Console console)
            throws Exception {
        console.println("Press 'q' for exit.");
        console.awaitInput('q');
    }

    private static JmsSyncEventProcessingApplication applicationFor(Configuration configuration, Console console) {
        ConnectionFactory connectionFactory = new PooledConnectionFactory(new ActiveMQConnectionFactory(configuration.brokerUrl));
        JmsConsumerService jmsConsumerService = new JmsConsumerService(connectionFactory, configuration.queueName, JMS_RECONNECT_ATTEMPT_INTERVAL);
        MftSyncEventProcessingService eventProcessingService = new MftSyncEventProcessingService();

        return new JmsSyncEventProcessingApplication(console, jmsConsumerService, eventProcessingService, configuration.maxProcessingAttempts);
    }

    /**
     * Creates a new application instance.
     *
     * @param console a console instance for application messages
     * @param jmsConsumerService a JMS consumer service
     * @param eventProcessingService a MFT event processing service
     * @param maxProcessingAttempts a max number of event processing attempts
     */
    public JmsSyncEventProcessingApplication(Console console,
                                             JmsConsumerService jmsConsumerService,
                                             MftSyncEventProcessingService eventProcessingService,
                                             int maxProcessingAttempts) {
        assertNotNull(console);
        this.console = console;

        assertNotNull(jmsConsumerService);
        this.jmsConsumerService = jmsConsumerService;

        assertNotNull(eventProcessingService);
        this.eventProcessingService = eventProcessingService;

        assertGreater(maxProcessingAttempts, 0, "Bad max processing attempts.");
        this.maxProcessingAttempts = maxProcessingAttempts;
    }

    @Override
    public void onMessage(JmsConsumerService service, Message message) {
        if (message instanceof ObjectMessage) {
            handle((ObjectMessage) message);
        } else {
            logUnsupportedMessage(message);
        }
    }

    /**
     * Starts the application.
     *
     * @throws Exception if an error occurs
     */
    public void start()
            throws Exception {
        start(null);
    }

    @Override
    protected void actualStart()
            throws Exception {
        this.eventProcessingService.start(null);
        this.jmsConsumerService.start(this);
    }

    @Override
    protected void actualStop() {
        this.jmsConsumerService.stop();
        this.eventProcessingService.stop();
    }

    @Override
    protected void actualDispose() {
        this.jmsConsumerService.dispose();
        this.eventProcessingService.dispose();
        logServiceDisposed();
    }

    private void handle(ObjectMessage message) {
        try {
            DomainEvent event = (DomainEvent) message.getObject();
            logIncomingEvent(event);

            for (int attempt = 1; attempt <= this.maxProcessingAttempts; ++attempt) {
                logProcessingAttempt(event, attempt);

                try {
                    this.eventProcessingService.handle(event);
                    logEventProcessed(event);

                    return;
                } catch (MftSyncEventProcessingService.EventProcessingException e) {
                    logEventProcessingFailed(event, e.triggerName);
                } catch (InterruptedException e) {
                    logEventProcessingInterrupted(event);
                    return;
                }
            }

            logMaxProcessingAttemptsExceeded(event);
        } catch (Exception e) {
            logError("Error processing message.", e);
        }
    }

    private void logServiceDisposed() {
        this.console.println("Application stopped.");
    }

    private void logIncomingEvent(DomainEvent event) {
        this.console.printf("Incoming MFT event: %s, id: '%s'.%n", event.getClass().getSimpleName(), event.getId());
    }

    private void logEventProcessed(DomainEvent event) {
        this.console.printf("MFT event processed: '%s'.%n", event.getId());
    }

    private void logEventProcessingFailed(DomainEvent event, String triggerName) {
        this.console.printf("Error processing MFT event: '%s', trigger name: '%s'.%n", event.getId(), triggerName);
    }

    private void logEventProcessingInterrupted(DomainEvent event) {
        this.console.printf("Error processing MFT event: '%s'. Processing interrupted.%n", event.getId());
    }

    private void logProcessingAttempt(DomainEvent event, int attempt) {
        this.console.printf("MFT event '%s' processing attempt %s.%n", event.getId(), attempt);
    }

    private void logMaxProcessingAttemptsExceeded(DomainEvent event) {
        this.console.printf("MFT event '%s' processing attempts exceeded.%n", event.getId());
    }

    private void logUnsupportedMessage(Message message) {
        this.console.printf("Unsupported JMS message: '%s'.%n", message);
    }

    private void logError(String message, Throwable error) {
        this.console.println(message);
        this.console.print(error);
        this.console.println();
        this.console.println();
    }

    /**
     * Application configuration.
     */
    public static class Configuration {

        private static final SingleOption SHOW_HELP_OPTION = new SingleOption("-h", false);

        private static final PairOption<String> BROKER_URL_OPTION = PairOption.stringOption("-url", true);
        private static final PairOption<String> QUEUE_OPTION = PairOption.stringOption("-queue", true);
        private static final PairOption<Integer> MAX_ATTEMPTS_OPTION = PairOption.integerOption("-maxattempts", false);

        private static final int DEFAULT_MAX_ATTEMPTS = 2;

        public static final String USAGE = "Usage:"
                                           + "%n"
                                           + "%n-url <URL> JMS broker URL"
                                           + "%n-queue <name> JMS queue name"
                                           + "%n[-maxattempts <value>] max event processing attempts"
                                           + "%n-h display this help menu"
                                           + "%n";

        public final String brokerUrl;
        public final String queueName;
        public final int maxProcessingAttempts;

        /**
         * Checks if help was requested in provided application arguments.
         *
         * @param arguments application arguments
         *
         * @return <code>true</code> if help was requested; <code>false</code> otherwise
         *
         * @throws InvalidArgumentsException if argument processing error occurs
         */
        public static boolean helpRequested(String[] arguments)
                throws InvalidArgumentsException {
            CommandLineService service = CommandLineService.defaultService();
            service.parse(arguments, SHOW_HELP_OPTION);

            return SHOW_HELP_OPTION.present();
        }

        /**
         * Creates a configuration instance for the provided application arguments.
         *
         * @param arguments application arguments
         *
         * @return an application configuration
         *
         * @throws InvalidArgumentsException if argument processing error occurs
         * @throws ConfigurationProperties.UnsupportedValueException if some argument has an unsupported format
         */
        public static Configuration configurationFor(String... arguments)
                throws InvalidArgumentsException,
                       ConfigurationProperties.UnsupportedValueException {
            CommandLineService service = CommandLineService.defaultService();

            service.parse(arguments, BROKER_URL_OPTION, QUEUE_OPTION, MAX_ATTEMPTS_OPTION);

            String brokerUrl = BROKER_URL_OPTION.value();
            String queueName = QUEUE_OPTION.value();
            int maxProcessingAttempts = MAX_ATTEMPTS_OPTION.present() ? MAX_ATTEMPTS_OPTION.value() : DEFAULT_MAX_ATTEMPTS;

            return new Configuration(brokerUrl, queueName, maxProcessingAttempts);
        }

        /**
         * Creates a new configuration instance.
         *
         * @param brokerUrl a JMS broker URL
         * @param queueName a JMS queue name
         * @param maxProcessingAttempts a max number of event processing attempts
         */
        public Configuration(String brokerUrl, String queueName, int maxProcessingAttempts) {
            assertNotNull(brokerUrl);
            this.brokerUrl = brokerUrl;

            assertNotNull(queueName);
            this.queueName = queueName;

            assertGreater(maxProcessingAttempts, 0, "Bad max processing attempts value.");
            this.maxProcessingAttempts = maxProcessingAttempts;
        }

        @Override
        public String toString() {
            return "Configuration {"
                   + "brokerUrl='" + this.brokerUrl + '\''
                   + ", queueName='" + this.queueName + '\''
                   + ", maxProcessingAttempts=" + this.maxProcessingAttempts
                   + '}';
        }
    }

}
