//------------------------------------------------------------------------------
// <copyright file="HelloWorldDurableCLI.cs" company="Microsoft">
//      Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
// <summary>
//      Sample Command Line Interface (CLI) demonstrating how to interact with
//      a custom SOA service (HelloWorldService) that runs Excel on a cluster.
//      Uses durable SOA sessions to allow broker failover.
// </summary>
//------------------------------------------------------------------------------

namespace HelloWorldDurableCLI
{
    using System;
    using System.ServiceModel;
    using System.Threading;
    using HelloWorldDurableCLI.HelloWorldService;
    using Microsoft.Hpc.Scheduler.Properties;
    using Microsoft.Hpc.Scheduler.Session;
    using Microsoft.Hpc.Scheduler.Session.Interface;
    using Microsoft.Hpc.Scheduler.Session.Internal;

    /// <summary>
    /// Class implementing Hello World sample client with durable sessions.
    /// </summary>
    public class HelloWorld
    {
        /// <summary>
        /// Try to reattach and/or recreate the session up to MAXRETRYCOUNT times 
        /// </summary>
        private static readonly int MAXRETRYCOUNT = 3;

        /// <summary>
        /// Session Identifier to allow reconnection. Default of -1 causes new session to be created.
        /// </summary>
        private static int sessionId = -1;

        /// <summary>
        /// Main method invoked by command line. Accepts cluster head node name and number of cores 
        /// along with a workbook path relative to the compute nodes. The name to say hello to and 
        /// number of hellos controls the contents and number of SOA messages.
        /// </summary>
        /// <param name="args">Command line parameters</param>
        public static void Main(string[] args)
        {
            // Syntax Usage Check 
            if (args.Length != 5 && args.Length != 2)
            {
                Console.WriteLine("To create a new session:");
                Console.WriteLine("HelloWorldDurableCLI <Workbook Path> <headnode> <Max Cores> <Recipient Name> <Number of Hellos>\n");
                Console.WriteLine("To reattach to an existing session:");
                Console.WriteLine("HelloWorldDurableCLI <headnode> <Session ID>");
                return;
            }

            // Durable session required for failover
            DurableSession session = null;
            BrokerClient<IHelloWorldService> client = null;

            // Initialize variables
            bool retryExceptionGenerated = true;
            int retryCount = 0;
            bool recollectOnly = false;
            int totalRequests = 0;
            string workbookPath = string.Empty;
            string headNode = string.Empty;
            string recipientName = string.Empty;
            int maxCores = 0;
            bool purge = true;

            // If initiating new session
            if (args.Length != 2)
            {
                // Set command line parameter variables
                workbookPath = args[0];
                headNode = args[1];
                recipientName = args[3];

                // Test workbook for existance
                if (!System.IO.File.Exists(workbookPath))
                {
                    Console.Error.WriteLine("Invalid workbook path: " + workbookPath);
                    return;
                }

                // Parse maximum number of cores
                if (int.TryParse(args[2], out maxCores))
                {
                    // Check for validity
                    if (maxCores <= 0)
                    {
                        Console.Error.WriteLine("Max cores must be greater than 0");
                        return;
                    }
                }
                else
                {
                    // Parsing Error
                    Console.Error.WriteLine("Invalid value for <Max Cores>: " + args[2]);
                    return;
                }

                // Parse number of requests
                if (int.TryParse(args[4], out totalRequests))
                {
                    // Check for validity
                    if (totalRequests <= 0 || totalRequests > 10000)
                    {
                        Console.Error.WriteLine("Number of hellos must be greater than 0 and less than 10000.");
                        return;
                    }
                }
                else
                {
                    // Parsing Error
                    Console.Error.WriteLine("Invalid value for <Number of Hellos>: " + args[4]);
                    return;
                }
            }
            else
            {
                // Try to reattach to an existing session to get results
                // Parse the session ID
                if (!int.TryParse(args[1], out sessionId))
                {
                    // Parsing Error
                    Console.Error.WriteLine("Invalid value for Session Id: " + args[0]);
                    return;
                }

                // Get head node to reattach to
                headNode = args[0];

                // remember that we are recollecting now
                recollectOnly = true;
            }

            // Try to reconnect on some failures
            while (retryExceptionGenerated && retryCount++ < MAXRETRYCOUNT)
            {
                try
                {
                    retryExceptionGenerated = false;
                    purge = true;

                    if (sessionId == -1)
                    {
                        // Get initialization information
                        SessionStartInfo info = InitSession(JobUnitType.Core, 1, maxCores, headNode);

                        // Create new session
                        session = DurableSession.CreateSession(info);
                        sessionId = session.Id;
                    }
                    else
                    {
                        session = DurableSession.AttachSession(new SessionAttachInfo(headNode, sessionId));
                    }

                    // Create client
                    client = new BrokerClient<IHelloWorldService>(session);

                    // Resend requests if they haven't all been sent.
                    if (client.GetStatus() == BrokerClientStatus.Ready)
                    {
                        // Send a new request for each hello
                        for (int recipientNumber = 1; recipientNumber <= totalRequests; recipientNumber++)
                        {
                            // For each request, send the recipient name and an identifier
                            SayHiRequest myReq = new SayHiRequest(workbookPath, recipientName, recipientNumber);
                            client.SendRequest(myReq);
                        }

                        // Complete batch of requests and wait for all respones to be received
                        client.EndRequests();
                        
                        // Prompt the user to decide whether to fire and recollect later or to just get the responses now
                        string choice;
                        do
                        {
                            Console.WriteLine("Would you like to receive responses now or reconnect later? (Now: 1, Later: 2) ");
                            choice = Console.ReadLine();
                        } 
                        while (choice != "1" && choice != "2");
                        
                        // If recollecting later, tell user how to recollect
                        if (choice.Equals("2"))
                        {
                            Console.WriteLine("When you want to recollect your responses, use this session ID: {0}", sessionId);
                            Console.WriteLine("(HelloWorldDurableCLI {0} {1})", headNode, sessionId);
                            purge = false;
                            return;
                        }
                    }

                    foreach (var response in client.GetResponses<SayHiResponse>())
                    {
                        // Extract hello message from response and print to command line
                        try
                        {
                            string message = response.Result.SayHiResult;
                            Console.WriteLine(message);
                        }
                        catch (FaultException ex)
                        {
                            // Print any application errors in the service
                            Console.Error.WriteLine("Fault Exception: " + ex.Message);
                        }
                        catch (Exception ex)
                        {
                            throw ex;
                        }
                    }
                    
                }
                catch (Exception ex)
                {
                    // Handle any error and decide whether to reattach, recreate, or give up
                    retryExceptionGenerated = ErrorHelper(ex);

                    // If reattaching and exception requires recreation of the session, give up
                    if (recollectOnly && sessionId == -1)
                    {
                        Console.Error.WriteLine("Error: Cannot recreate session when reconnecting");
                        retryExceptionGenerated = false;
                    }

                    // Purge when not retrying
                    purge = !(retryExceptionGenerated && retryCount < MAXRETRYCOUNT);
                }
                finally
                {
                    try
                    {
                        if (client != null)
                        {
                            // Only purge the client when not recollecting only
                            client.Close(purge && !recollectOnly);
                            client = null;
                        }

                        if (session != null)
                        {
                            // Only purge the session when not retrying (after unrecoverable failure or successful completion)
                            session.Close(purge);
                            session = null;
                        }
                    }
                    catch (Exception ex)
                    {
                        // Notify the user when there is an error cleaning up the cluster.
                        Console.Error.WriteLine("Failure while cleaning up cluster resources: {0}", ex.Message);
                    }
                }
            } // Failover retry loop
        } // main

        /// <summary>
        /// Initialize the session start information with supplied values and defaults
        /// </summary>
        /// <param name="type"> granularity of resource requested </param>
        /// <param name="min"> minimum number of resource units </param>
        /// <param name="max"> maximum number of resource units </param>
        /// <param name="headNode"> name of the head node </param>
        /// <returns> start information for session creation </returns>
        private static SessionStartInfo InitSession(JobUnitType type, int min, int max, string headNode)
        {
            // Use HelloWorldService for sample
            SessionStartInfo info = new SessionStartInfo(headNode, "HelloWorldService");
            info.ResourceUnitType = type;
            info.MinimumUnits = min;
            info.MaximumUnits = max;
            info.Secure = true;

            return info;
        } // InitSession

        /// <summary>
        /// Method to handle error conditions appropriately
        /// </summary>
        /// <param name="ex"> Exception to handle </param>
        /// <returns> Whether or not to retry the connection </returns>
        private static bool ErrorHelper(Exception ex)
        {
            bool retryExceptionGenerated = false;
            if (ex is SessionException)
            {
                // On a session exception, there are three classes of failures. Some failures likely result from a connection problem 
                // with the existing session. In these cases, reattaching to the current session may be possible. Other failures likely
                // result from a failure in the session, which means that a new session is required in order to retry. Finally, there
                // are some failures that point to a problem with cluster or service installation. Retrying on these types of errors would not have 
                // any chance of succeeding. This sample attempts to reconnect or recreate when there is a chance of success, but this
                // is not the desired course of action for all applications. Consider your use cases when deciding how to handle various errors.
                SessionException tempEx = (SessionException)ex;
                if (((uint)(tempEx.ErrorCode) & (uint)(SOAFaultCodeCategory.SessionConnectionError)) == (uint)(SOAFaultCodeCategory.SessionConnectionError))
                {
                    // If there is a connection error, try to reattach
                    retryExceptionGenerated = true;
                    Console.WriteLine("\nAttempting to reconnect to existing session after: \n{0}", ex.Message);
                }
                else if (((uint)(tempEx.ErrorCode) & (uint)(SOAFaultCodeCategory.SessionError)) == (uint)(SOAFaultCodeCategory.SessionError))
                {
                    // If there is a session error, retry creating the session.
                    retryExceptionGenerated = true;
                    sessionId = -1;
                    Console.WriteLine("\nAttempting to recreate the session after: \n{0}", ex.Message);
                }
                else
                {
                    // Do not retry on any other category of exception
                    retryExceptionGenerated = false;
                    Console.Error.WriteLine("\nFatal session exception: {0}", ex.Message);
                }
            }
            else if (ex is CommunicationException || ex is TimeoutException)
            {    
                    // Try to reconnect after timeout or communication exception
                    Console.WriteLine("\nAttempting to reconnect to existing session after: \n{0}", ex.Message);
                    retryExceptionGenerated = true;
            }
            else
            {
                // Do not try to reconnect on other exceptions
                Console.WriteLine("\nFatal exception: {0}", ex.Message);
            }

            return retryExceptionGenerated;
        }
    } // Class
} // Namespace
