using System;
using System.Diagnostics;
using System.Linq;
using System.ServiceProcess;
using System.Reflection;
using PlutoServer.MSL.Connectors;
//using TCMPortMapper;
using System.Net;
using Pluto.Api;
using System.Xml;
using System.IO;
using System.Text;
using Sdaleo.Systems.Advantage;
using Sdaleo;
using Sdaleo.Systems.SQLServer;
using Yaulw.File;
using Yaulw.Thread;
using TCMPortMapper;
using Yaulw.Assembly;

namespace PlutoServer.MSL
{
    public partial class PlutoService : ServiceBase
    {
        //Portmapper singleton instance
        PortMapper pMapper = PortMapper.SharedInstance;

        #region Internal Consts

        // Hard-coded in * Internal channel always uses these ports
        internal const int INTERNAL_CHANNEL_PORT = 1945;
        internal const int EXTERNAL_MINIMUM_PORT_NUMBER = 49000;
        internal const int EXTERNAL_MAXIMUM_PORT_NUMBER = 50000;

        // Hard-coded in * Registry Strings / Settings        
        internal const string LOCAL_MACHINE_SUBKEY = "Software\\MSLMobile";
        internal const string LOCAL_MACHINE_EXTERNALPORT = "ExternalPort";
        internal const string LOCAL_MACHINE_HOSTGUID = "HostGuid";
        internal const string LOCAL_MACHINE_INITIALSETUPDT = "InitialSetup";


        // Threshold used to determine if system just rebooted, and service just
        // started. We need to wait a little because we need to make sure SQLServer
        // and Advantage Server are up
        internal const int NUMBER_OF_MINUTES_SERVICE_UP_THRESHOLD = 5;
        internal const int NUMBER_OF_MINUTES_SYSTEM_UP_THRESHOLD = 10;

        // Threshold used to query WhatIsMyIP (they allow every 5 Minutes)
        internal const int NUMBER_OF_MINUTES_TO_QUERY_FOR_EXTERNAL_IP = 15;

        // Use this host to check, if we are on the McKesson Network
        internal const string MCK_HOST_TO_CHECK_ON_MCK_NETWORK = "ndh1fs01.mckesson.com";

        #endregion

        #region Internal Statics (Important for getting Information from Pluto Service)

        /// <summary>
        /// Service Start Time
        /// </summary>
        internal static DateTime ServiceStartDT = DateTime.MinValue;

        /// <summary>
        /// Did the service statup on the McKesson Network? Initialized upon Service Start
        /// </summary>
        internal static bool IsOnMcKessonNetwork { get; private set; }

        /// <summary>
        /// Are the External IP and Port Loaded (and set)
        /// </summary>
        internal static bool IsExtIPandExtPortSet { get { return s_PlutoService.InitialExternalIPConfigurationIsSet; } }

        /// <summary>
        /// LocalIPAddress, automatically set by our Connectivity Thread
        /// </summary>
        internal static IPAddress InternalIP { get; private set; }

        /// <summary>
        /// ExternalIPAddress, automatically set by our Connectivity Thread
        /// updated upon service startup and then at interval NUMBER_OF_MINUTES_TO_QUERY_FOR_EXTERNAL_IP
        /// </summary>
        internal static IPAddress ExternalIP { get; private set; }

        /// <summary>
        /// External Port, automatically set by our Connectivity Thread (Radomly or from the Registry)
        /// LOCAL_MACHINE_EXTERNALPORT
        /// </summary>
        internal static uint ExternalPort { get; private set; }

        /// <summary>
        /// There is no need for us to update the server if the Host GUID doesn't exist, hence
        /// we need to at start up know whether or not to automatically update or not, or whether to
        /// just wait
        /// </summary>
        internal static RegistrationWrapper.HostGUIDstate state = RegistrationWrapper.HostGUIDstate.no_connectivity;

        /// <summary>
        /// Quick Check to get the local IP Address
        /// </summary>
        /// <returns></returns>
        internal static IPAddress IPFetchLocalIPAddress
        {
            get
            {
                IPAddress IP = Yaulw.Net.IPHostHelper.GetFirstLocalIPAddress();
                if (IP == IPAddress.None)
                    MSLSpecific.Logger.Error("No Local IP Address Found using IP {0} as Local Address", IPAddress.None);
                return IP;
            }
        }

        /// <summary>
        /// Reloads the External IP, and Port Configuration,
        /// by setting ReloadExternalIPandPortConfiguration to true
        /// </summary>
        internal static bool ReloadExtIPandPortConfiguration(int Port)
        {
            if (Port > 0 && Port != ExternalPort)
            {
                s_PlutoService.DeactivateROExternalChannelIfItHasBeenActivated(false);

                // Set the Port in the Registry
                Yaulw.Registry.RegKey.SetKey<int>(Yaulw.Registry.HKEYRoot.LocalMachine, LOCAL_MACHINE_SUBKEY, LOCAL_MACHINE_EXTERNALPORT, Port);

                // Reload the Ports
                IPAddress extIP = IPAddress.None;
                uint nPort = 0;

                // A user could already have the router pointed to us while we are still set on another port,
                // then decide to change port. We can check via PlutoCheck if this new port is actually pointing to us already
                // without the Pluto check we will see it as used and jump to another port (unexpected behavior for the user)
                if (s_PlutoService.Retrieve_ExternalIP_ExternalPort(true, true, true, out extIP, out nPort))
                {
                    ExternalIP = extIP;
                    ExternalPort = nPort;
                    s_PlutoService.LastExternalIPCheckRun = DateTime.Now;

                    // Re-initialize the McKesson's Mobile Gateway
                    RegistrationWrapper.UpdateServerIntExt(InternalIP, ExternalIP, ExternalPort);

                    // Re-activate the Channel
                    s_PlutoService.ActivateROInternalChannelIfHasNotBeenActivated();

                    // If the port is now what the user asked for than we are good to go
                    bool bSuccess = (nPort == Port);
                    if (!bSuccess)
                        MSLSpecific.Logger.Error("ReloadExtIPandPortConfiguration Failed nPort:{0} Port:{1}", nPort, Port);
                    return bSuccess;
                }
            }
            return false;
        }        

        #endregion

        #region Private Members

        /// <summary>
        /// Check if this is the first time the network got enabled
        /// </summary>
        private bool FirstTimeNetworkConnected = true;

        /// <summary>
        /// Was the Internal Channel Activated?
        /// </summary>
        private bool ROInternalHasBeenActivated = false;

        /// <summary>
        /// Was the External Channel Activated?
        /// </summary>
        private bool ROExternalHasBeenActivated = false;

        /// <summary>
        /// Did a System Reboot just occur?
        /// </summary>
        private bool SystemJustStarted_SoUseServiceThreshold = false;
        private bool ServiceUpTimeReached = false;
        private bool SystemThresholdNotReached = true;

        /// <summary>
        /// Has the DataStore been read in? Try up to Number of Times 
        /// </summary>                
        private bool DataStoreHasBeenRead = false;
        private uint Number_Of_Tries_to_Read_DataStore = 3;
        private bool DataStoreReadErroredOut = false;

        /// <summary>
        /// Has the PortMapper been started?
        /// </summary>                
        private bool IsPortMapperRunning = false;

        /// <summary>
        /// Is the Logger Setup?
        /// </summary>
        private bool LoggerIsSetup = false;

        /// <summary>
        /// Try to Read in External IP configuration. Try up to Number of Times
        /// </summary>
        private bool InitialExternalIPConfigurationIsSet = false;
        private uint Number_Of_Tries_to_Read_ExternalIP_Configuration = 3;
        private bool InitialExternalIPRetrieveErrorerOut = false;

        /// <summary>
        /// What is my IP only allows us to check external IP every NUMBER_OF_MINUTES_TO_QUERY_FOR_EXTERNAL_IP
        /// </summary>
        private DateTime LastExternalIPCheckRun = DateTime.MinValue;

        /// <summary>
        /// Our Connectivity Timer Thread
        /// </summary>
        private SingleThreadTimer connectivityTimer = null;

        /// <summary>
        /// Allows us to skip cycles in the Connectivity Thread
        /// </summary>
        private uint nConnectivitySkipCounter = 0;

        /// <summary>
        /// If for some reason the network goes down, we should re-initialize
        /// everything as if nothing happened
        /// </summary>
        private bool NetworkOutageDetected = false;

        /// <summary>
        /// When we start up we have no knowledge whether we can connect to the
        /// registrat/gateway server. Also if it goes down. If we detect a switch from
        /// no to exist we force an external UpdateServer Refresh
        /// *by default this is true* because at startup we want to do this also
        /// </summary>
        private bool SwitchFromNoConnectivityToConnectivityOccured = true;

        /// <summary>
        /// Send the service version upon Service start-up
        /// </summary>
        private bool ServiceStartVersionSent = false;

        #endregion

        #region Construction

        /// <summary>
        /// Singleton Instance of the Service
        /// </summary>
        private static PlutoService s_PlutoService = null;

        /// <summary>
        /// Constructor
        /// </summary>
        public PlutoService()
        {
            InitializeComponent();
            
            // Log to the Application System Log
            this.EventLog.Log = "Application";
            s_PlutoService = this;

            // Handle all unexpected errors
            AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
        }

        /// <summary>
        /// Unhandled Domain Exception
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
        {
            Exception ex = (Exception)e.ExceptionObject;
            String msg = "UnhandledException Occured: " + ex.Message + "\n\n" + ex.InnerException.Message;
            AppLogError(msg);
        }

        #endregion

        #region Windows Event Logging

        /// <summary>
        /// Log into the Event Viewer as well as the Service Log (if possible)
        /// </summary>
        /// <param name="errMsg"></param>
        internal static void AppLogError(string errMsg)
        {
            s_PlutoService.EventLog.WriteEntry(errMsg, EventLogEntryType.Error);
            if (MSLSpecific.Logger != null)
                MSLSpecific.Logger.Error(errMsg);
        }

        /// <summary>
        /// Log into the Event Viewer as well as the Service Log (if possible)
        /// </summary>
        /// <param name="errMsg"></param>
        internal static void AppLogInfo(string infoMsg)
        {
            s_PlutoService.EventLog.WriteEntry(infoMsg, EventLogEntryType.Information);
            if (MSLSpecific.Logger != null)
                MSLSpecific.Logger.Info(infoMsg);
        }

        /// <summary>
        /// Log into the Event Viewer as well as the Service Log (if possible)
        /// </summary>
        /// <param name="errMsg"></param>
        internal static void AppLogWarning(string warnMsg)
        {
            s_PlutoService.EventLog.WriteEntry(warnMsg, EventLogEntryType.Warning);
            if (MSLSpecific.Logger != null)
                MSLSpecific.Logger.Info(warnMsg);
        }

        #endregion

        // usefull for debugging Service in visual studio
#if DEBUG
        internal void DebugStart(string[] args)
        {            
            DEBUGGING_ONLY.DEBUGSTEP_INTO();
            OnStart(args);
        }
#endif

        #region System / Service Up Time

        /// <summary>
        /// Get Service UpTime
        /// </summary>
        /// <returns></returns>
        private TimeSpan GetServiceUpTime()
        {
            return (DateTime.Now - ServiceStartDT);
        }

        /// <summary>
        /// For Service Handling, see if the system has been up long enough
        /// Once it becomes true, it will always return true
        /// </summary>
        private bool HasServiceBeenUpLongEnough()
        {
            if (!ServiceUpTimeReached)
                ServiceUpTimeReached = GetServiceUpTime().TotalMinutes >= NUMBER_OF_MINUTES_SERVICE_UP_THRESHOLD;
            return ServiceUpTimeReached;
        }

        /// <summary>
        /// For Error Handling, see if the system has been up till this threshold
        /// Once it becomes false, will always return false
        /// </summary>
        /// <returns></returns>
        private bool IsSystemBeenUpWithinMaxThreshold()
        {
            if (SystemThresholdNotReached)
                SystemThresholdNotReached = (Yaulw.Installer.Common.GetSystemUpTime().TotalMinutes <= NUMBER_OF_MINUTES_SYSTEM_UP_THRESHOLD);
            return SystemThresholdNotReached;
        }

        #endregion

        /// <summary>
        /// Service Start-Up
        /// </summary>
        /// <param name="args"></param>
        protected override void OnStart(string[] args)
        {
            try
            {
                // Service Started                                                    
                // System just started, so make sure that service is timing out a little to make
                // sure everything is up and running            
                SystemJustStarted_SoUseServiceThreshold = IsSystemBeenUpWithinMaxThreshold();
                AppLogInfo(String.Format("MSLMobile Api Service version:'{0}' started up at:'{1}'. System Reboot detected:'{2}'", AssemblyW.GetAssemblyVersion(AssemblyW.AssemblyST.Entry).ToString(),  DateTime.Now.ToLongTimeString(), SystemJustStarted_SoUseServiceThreshold));
                ServiceStartDT = DateTime.Now;

                // Are we on the McKesson Network?
                IsOnMcKessonNetwork = Yaulw.Net.IPHostHelper.CanResolveHost(PlutoService.MCK_HOST_TO_CHECK_ON_MCK_NETWORK, true);
                if (IsOnMcKessonNetwork)
                    AppLogInfo("McKesson Network Detected");
                else
                    AppLogInfo("Regular Network Detected");

                // Load the AppConfig
                if(Configuration.Load(false))
                   AppLogInfo("Using the .config File as main configuration");
                else
                   AppLogInfo("Using hard-coded in values as main configuration");

                // Load the Determine External IP Url
                Yaulw.Net.IPHostHelper.WHAT_IS_MY_IP_AUTOMATION_URL = Configuration.DETERMINE_EXTERNAL_IP_ADDRESS_URL;
                AppLogInfo(String.Format("Using '{0}' as our determine external IP address url", Configuration.DETERMINE_EXTERNAL_IP_ADDRESS_URL));

                // Internet / Network Connectivity Timer
                // * Ran into issues when computer is rebooted, we need certain things working before the service can start, 
                // * therefore decided to put DataStore Read(), AutoUpdate, RO Connection into a Connection Timer,
                // that keeps checking every 10 seconds if we have network connectivity * Enables / Disables RO Channels as needed, just in case *
                AppLogInfo("MSLMobile Api Service starting connectivity timer.");
                connectivityTimer = new SingleThreadTimer(ConnectivityHandler, (uint)TimeSpan.FromSeconds(10).TotalMilliseconds, true);
            }
            catch (Exception e)
            {
                AppLogError(String.Format("Service OnStart() Error thrown. Fatal. Can't continue: {0}", e.Message));
                this.Stop();
            }
        }

        #region Private Connectivity Timed Thread (runs every 10 Seconds)

        /// <summary>
        /// Single Threaded Connectivity Handler (Handles Auto-Update, ReadInDataStore(), and RO Channels) 
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ConnectivityHandler(object sender, Yaulw.Thread.SingleThreadTimer.STElapsedEventArgs e)
        {
            // System just started, we won't do anyting in here until our
            // Service Startup 'NUMBER_OF_MINUTES_SERVICE_UP_THRESHOLD' timeout of is reached
            if (SystemJustStarted_SoUseServiceThreshold && !HasServiceBeenUpLongEnough())
                return;

            // If the skip counter is set, use it to skip iterations
            if (nConnectivitySkipCounter > 0)
            {
                nConnectivitySkipCounter--;
                return;
            }

            // Check Connectivity,
            // ~withouth basic networking being up there is nothing really to do here
            bool bHasConnection = Yaulw.Net.IPHostHelper.HasConnection();
            if (!bHasConnection)
            {
                DeactivateROChannelsIfTheyHaveBeenActivated(true);
                return;
            }

            // Initialize Internal IP
            if (InternalIP == null)
                InternalIP = PlutoService.IPFetchLocalIPAddress;

            // Always set up the Logger First (Needed for all subsequent Calls)
            if (!LoggerIsSetup)
            {
                AppLogInfo("Setting up Logger");
                MSLSpecific.Setup_Logger();
                LoggerIsSetup = true;
            }

            // Check To See if we need to update, This is our first time that
            // the system has been up long enough and that we have a network connection
            if (FirstTimeNetworkConnected)
            {
                AppLogInfo("System has been up long enough. Starting Initialization.");
                // Upon Every Startup of the Service, let Auto-Update know to update the service when needed
#if !DEBUG
                AppLogInfo("Starting Auto-Update Timer.");
                AutoUpdate.StartAutoTimer_ServiceStarted();
#endif
                FirstTimeNetworkConnected = false;
                // just to make sure wait another cycle * due to Auto-Update * Feature
                return;
            }

            // * Check to Make sure the System has been up long enough *
            // Calling ReadInDataStore() requires Advantage and/or SQL Server to be up
            // and running and hence, we can't do it right at service start when the
            // server just rebooted, we wait 5 of service uptime to make sure after a server reboot, 
            // that the services needed are up
            if (!FirstTimeNetworkConnected)
            {                
                // If Network disconnection occured, we should try to re-read in everything,
                // to make sure we are doing the best we can
                if (NetworkOutageDetected)
                {
                    AppLogInfo("Network Outage Detected - Rereading Data and External IP settings");

                    // Re-read in Data Store
                    DataStoreHasBeenRead = false;
                    DataStoreReadErroredOut = false;

                    // Re-initialize initial ExternalIP Retrival
                    InitialExternalIPConfigurationIsSet = false;
                    InitialExternalIPRetrieveErrorerOut = false;
                }

                // First step try to load the DataBase Cache
                if (!LoadInDataStore())
                    return;

                // Second is to do is to figgure out the external IP and Port Settings
                if (!Initialize_ExternalIP_And_ExternalPort())
                    return;

                // Lastly, activate any of the RO Channels
                ActivateROChannelsIfTheyHaveNotBeenActivated();

                // There is no need to call update Server automatically,
                // if the host guid doesn't exist, the MSLSpecific calls would
                // have to call RegisterNewPractice First (in case of no connectivity
                // we can't call registration server anyways, so may as well time out
                // for 5 minutes and try again)
                switch (state)
                {
                    case RegistrationWrapper.HostGUIDstate.no_connectivity:
                        {
                            state = RegistrationWrapper.DoesServerHostGuidExist();
                            if (state == RegistrationWrapper.HostGUIDstate.no_connectivity)
                            {
                                MSLSpecific.Logger.Info("Communication to Mobile Gateway Server could not be established, trying again in 5 minutes.");
                                // try again in 5 minutes...
                                nConnectivitySkipCounter = 30;
                                // force a UpdateServerIntExt right away once connectivity is restored
                                SwitchFromNoConnectivityToConnectivityOccured = true;
                                return;
                            }
                            else if (state == RegistrationWrapper.HostGUIDstate.exists)
                            {
                                goto case RegistrationWrapper.HostGUIDstate.exists;
                            }
                            break;
                        }
                    case RegistrationWrapper.HostGUIDstate.exists:
                        {
                            // Try to force the UpdateServerIntExt Right away,
                            // once a switch from no connectivity to connectivity occurs
                            if (SwitchFromNoConnectivityToConnectivityOccured &&
                                InitialExternalIPConfigurationIsSet)
                            {
                                RegistrationWrapper.UpdateServerIntExt(InternalIP, ExternalIP, ExternalPort);
                                SwitchFromNoConnectivityToConnectivityOccured = false;
                            }
                            else if (SwitchFromNoConnectivityToConnectivityOccured)
                            {
                                // We must also initialize internal, in case external was never set
                                // i.e. we are on the McKesson Network or WIFI only mode
                                RegistrationWrapper.UpdateServerInternal(InternalIP);
                                SwitchFromNoConnectivityToConnectivityOccured = false;
                            }

                            // Let the Gateway know upon service start-up what version we are running 
                            // (only do this once per service start), so after a fresh install, we know what they are running
                            if (!ServiceStartVersionSent)
                            {                            
                                Version LocalVersion = Yaulw.Assembly.AssemblyW.GetAssemblyVersion(Yaulw.Assembly.AssemblyW.AssemblyST.Entry);
                                RegistrationWrapper.ServerHasBeenUpdated(LocalVersion.ToString());
                                ServiceStartVersionSent = true;
                            }

                            // Handle Internal Address Changes every 10 seconds
                            HandleLocalIPAddressChange();

                            // Handle External Address Change every 5 minutes
                            HandleExternalIPAddressChange();
                            break;
                        }
                    default:
                        {                            
                            break;
                        }
                }
            }
        }

        #endregion

        #region Connectivity Important Handlers

        /// <summary>
        /// First_LoadInDataStore() First thing to do, we should connect to the data store,
        /// we try up to 3 times, should be enough. Each time has 1 minute.
        /// </summary>
        /// <param name="bHasConnection">true, if network connection is up</param>
        /// <returns>True for the Caller to continue, false otherwise</returns>
        private bool LoadInDataStore()
        {
            if (!DataStoreHasBeenRead && !DataStoreReadErroredOut)
            {
                AppLogInfo("Trying to load the DataStore");
                DataStoreHasBeenRead = DBCache.ReadInDataStore();
                if (DataStoreHasBeenRead)
                {
                    // This is good news!
                    AppLogInfo("DataStore has been read successfully or DataStore is empty");
                    return true;
                }
                else if (!DataStoreHasBeenRead && Number_Of_Tries_to_Read_DataStore > 0)
                {
                    // We are still erroring out, try again in a minute, up to Number_Of_Tries_to_Read_DataStore
                    // Skip six cycles, aka. 60 seconds for each try
                    Number_Of_Tries_to_Read_DataStore--;
                    nConnectivitySkipCounter = 6;

                    // Don't allow to continue until this is complete
                    return false;
                }
                else if (!DataStoreHasBeenRead && Number_Of_Tries_to_Read_DataStore == 0)
                {
                    DataStoreReadErroredOut = true;
                    AppLogWarning("Failed to read a single DataStore item. Mobile Api Service may not be in a good state. Contact support if problem persist.");

                    // Failed to many times, warn the user but nothing totally critical yet
                    return true;
                }
            }

            // always continue, once we reach here
            return true;
        }

        /// <summary>
        /// Trues to retrieve and set the External IP, and calculatest a possible Port To Use,
        /// we try up to 3 times, should be enough. Each time has 1 minute.
        /// </summary>
        /// <param name="bHasConnection">true, if network connection is up</param>
        /// <returns>True for the Caller to continue, false otherwise</returns>
        private bool Initialize_ExternalIP_And_ExternalPort()
        {
            try
            {
#if DEBUG
                if (!InitialExternalIPConfigurationIsSet && !InitialExternalIPRetrieveErrorerOut)
#else
                if (!IsOnMcKessonNetwork && !InitialExternalIPConfigurationIsSet && !InitialExternalIPRetrieveErrorerOut)
#endif
                {
                    AppLogInfo("Trying to load External IP Configuration");
                    IPAddress extIP = IPAddress.None;
                    uint Port = 0;

                    // During initialization/startup we just will only check that the port is not in use via
                    // Port Check. A pluto check is overkill and doesn't make sense. Only a port check should be enough.
                    // Whether this get's called when network card is down or upon Service Startup, we will be fine
                    // by auto-calculating the port and not pluto checking
                    if (Retrieve_ExternalIP_ExternalPort(true, true, false, out extIP, out Port))
                    {
                        InitialExternalIPConfigurationIsSet = true;
                        ExternalIP = extIP;
                        ExternalPort = Port;
                        LastExternalIPCheckRun = DateTime.Now;
                        AppLogInfo(String.Format("Loaded initial External IP:{0} and Port:{1} at {2}", extIP, ExternalPort, LastExternalIPCheckRun));
                        return true;
                    }
                    else if (!InitialExternalIPConfigurationIsSet && Number_Of_Tries_to_Read_ExternalIP_Configuration > 0)
                    {
                        // We are still erroring out, try again in a minute, up to Number_Of_Tries_to_Read_ExternalIP_Configuration
                        // Skip six cycles, aka. 60 seconds for each try
                        Number_Of_Tries_to_Read_ExternalIP_Configuration--;
                        nConnectivitySkipCounter = 6;

                        // Don't allow to continue until this is complete
                        return false;
                    }
                    else if (!InitialExternalIPConfigurationIsSet && Number_Of_Tries_to_Read_ExternalIP_Configuration == 0)
                    {
                        // Failed too many times, we can work arround this via *Wifi Only*
                        InitialExternalIPRetrieveErrorerOut = true;
                        AppLogWarning("Failed to load External IP Configuration for now. Will try again in 5 minutes");

                        // Failed to many times, warn the user but nothing totally critical yet
                        LastExternalIPCheckRun = DateTime.Now;
                        return true;
                    }
                }

                // always continue, once we reach here
                return true;
            }
            catch (Exception e)
            {
                MSLSpecific.Logger.Error("Something is seriously wrong with Initialize_ExternalIP_And_Port.", e);
                MSLSpecific.Logger.Fatal("Dev has to look into this. Program may not be able to recover.", e);
            }
            return false;
        }

        /// <summary>
        /// Handle Local IP Address Change
        /// </summary>
        private void HandleLocalIPAddressChange()
        {
            IPAddress intIP = IPAddress.None;
            bool bChangeOccured = Yaulw.Net.IPHostHelper.AreIPAddressesDifferent(InternalIP, PlutoService.IPFetchLocalIPAddress, out intIP);
            if (bChangeOccured && Yaulw.Net.IPHostHelper.IsValidIPv4Address(intIP, false))
            {
                InternalIP = intIP;
                if (InitialExternalIPConfigurationIsSet)
                {
                    // Update the McKesson's Mobile Gateway
                    RegistrationWrapper.UpdateServerIntExt(InternalIP, ExternalIP, ExternalPort);
                }
                else
                {
                    // Update the McKesson's Mobile Gateway
                    RegistrationWrapper.UpdateServerInternal(InternalIP);
                }
            }
        }

        /// <summary>
        /// Handle External IP Address Change
        /// </summary>
        private void HandleExternalIPAddressChange()
        {
            // What is my ip automation rules allow every NUMBER_OF_MINUTES_TO_QUERY_FOR_EXTERNAL_IP minutes to check, so that is what'll do
            TimeSpan ts = DateTime.Now - LastExternalIPCheckRun;
            bool bTimePassed = (ts.TotalMilliseconds >= TimeSpan.FromMinutes(NUMBER_OF_MINUTES_TO_QUERY_FOR_EXTERNAL_IP).TotalMilliseconds);

            // Also Check that we are not updating between the hours of 2am to 6am
            DateTime dt2 = DateTime.Now.ToLocalTime();
            TimeSpan ts2 = dt2.TimeOfDay;
            bool bIsBetween2amAnd6am = (ts2.TotalHours >= 2.0) && (ts2.TotalHours <= 6.0);
            if (bIsBetween2amAnd6am && bTimePassed)
                bTimePassed = false;

            // We need to automatically keep track of the external IP on External Networks
            // and update it with the McKesson's Mobile Gateway, as needed
#if DEBUG
            if (bTimePassed)
#else
            if (!IsOnMcKessonNetwork && bTimePassed)
#endif
            {
                // Retrieve both Port and IP configuration to set the initial configuration
                if (!InitialExternalIPConfigurationIsSet)
                {
                    IPAddress extIP = IPAddress.None;
                    uint Port = 0;

                    // For some reason the InitialExternalIPConfiguration was never set, which means
                    // the port also was never calculated / used. Because we need an external ip to check the external port. 
                    // try to retrieve the external ip and do a port check , don't force multiple checks and don't do a pluto check on
                    // the port (that will only be done via ReloadExIPandPortConfiguration())                    
                    if (Retrieve_ExternalIP_ExternalPort(false, true, false, out extIP, out Port))
                    {
                        InitialExternalIPConfigurationIsSet = true;
                        ExternalIP = extIP;
                        ExternalPort = Port;
                        LastExternalIPCheckRun = DateTime.Now;
                        AppLogInfo(String.Format("Loaded External IP:{0} and Port:{1} at {2}", extIP, ExternalPort, LastExternalIPCheckRun));
                        
                        // Initialize the McKesson's Mobile Gateway
                        RegistrationWrapper.UpdateServerIntExt(InternalIP, ExternalIP, ExternalPort);
                    }
                }
                else 
                {
                    // We only check if the External IP has changed. The Port will only change if the user
                    // modifies the registry and reboots the server or the user calls ReloadExtIPandPortConfiguration()
                    // via the Diagnose Mobile Tool. Hence we don't want to do any port checking here at all
                    IPAddress extIP = Yaulw.Net.IPHostHelper.GetExternalIP();
                    if (extIP != IPAddress.None)
                    {
                        bool bChangeOccured = Yaulw.Net.IPHostHelper.AreIPAddressesDifferent(ExternalIP, extIP, out extIP);
                        if (bChangeOccured && Yaulw.Net.IPHostHelper.IsValidIPv4Address(extIP, false))
                        {
                            ExternalIP = extIP;
                            LastExternalIPCheckRun = DateTime.Now;
                            AppLogInfo(String.Format("Updated External IP:{0} at {1}", extIP, LastExternalIPCheckRun));

                            // Update the McKesson's Mobile Gateway
                            RegistrationWrapper.UpdateServerIntExt(InternalIP, ExternalIP, ExternalPort);
                        }
                    }
                }
            }
        }

        #endregion

        #region Private Helper Functions

        /// <summary>
        /// Core logic to retrieve External IP and Port to use (should be called only when not on McKesson Network)
        /// </summary>
        /// <param name="ExternalIP">External Host IP</param>
        /// <param name="bForceMultipleExternalChecks">set to true during initialization of the service, to check more than once, to make sure</param>
        /// <param name="bCheckPort">if set to true, will perform Port Sockets Checks to make sure the port isn't used, if false won't do any port checking and just use what is given * should be set to true upon initialization *</param>
        /// <param name="bPlutoCheckPort">if bCheckPort is set to true and we encounter an open port will perform a pluto check on it and see if it's hostguid matches ours, if so, decides we can use this port * RARE Reason this should ever be set to true *</param>
        /// <param name="Port">Random Port that can be used</param>
        /// <returns>true if all of our parameters were passed out and valid, false otherwise</returns>
        private bool Retrieve_ExternalIP_ExternalPort(bool bForceMultipleExternalChecks, bool bCheckPort, bool bPlutoCheckPort, out IPAddress ExternalIP, out uint Port)
        {
            ExternalIP = IPAddress.None;
            Port = 0;

            // First Try, is to use the Port that is either random or saved in the registry,
            // if that fails (which the odds are pretty low), we fall back to a second try,
            // which will be random for sure. But just in case, let's do it a 3rd Random time,
            // if bForceMultipleExternalChecks is set to true.
            int nTrys = bForceMultipleExternalChecks ? 3 : 2;

            // Try to Fetch the External IP, this should be easy, one would think            
            bool bFetchedExternalIP = false;
            for (int n = 0; n < nTrys && !bFetchedExternalIP; n++)
            {
                // Try to get the external IP (should work most of the time, if we have internet)
                // But once we have it, we shouldn't have to try 3 times
                if (!bFetchedExternalIP)
                {
                    ExternalIP = Yaulw.Net.IPHostHelper.GetExternalIP();
                    if (ExternalIP != IPAddress.None)
                    {
                        bFetchedExternalIP = true;
                        MSLSpecific.Logger.Info("External IP Fetched:{0}", ExternalIP.ToString());
                    }
                }
            }

            // Try to find a suitable Port, by trying to 
            // Get the Port Number (should always work), but we
            // need to make sure that the port is free to use
            bool bFoundFreePort = false;
            for (int n = 0; n < nTrys && !bFoundFreePort && bFetchedExternalIP && bCheckPort; n++)
            {
                Port = (uint)GetRandomOrSavedPortNumber(n > 0);                
                MSLSpecific.Logger.Info("Checking Port:{0} to use", Port);

                // Check to see if the port is open via external IP                                
                bool bIsReachable = RegistrationWrapper.IsServerReachable(ExternalIP, Port);
                if (bIsReachable)
                {
                    if (bPlutoCheckPort)
                    {
                        string FoundHostGuid = RegistrationWrapper.RetrieveHostGUIDForServer(ExternalIP, Port);
                        if (!String.IsNullOrEmpty(FoundHostGuid))
                        {
                            // for whatever miracelous reason this is us (maybe thru registry)
                            if (String.Compare(FoundHostGuid, MSLSpecific.GetHostGUID().ToString(), true) == 0)
                                bFoundFreePort = true;
                        }
                    }
                }
                else
                {
                    // Noting Reachable, assume we can use it. (DiagnoseMobile Tool can later make changes, if needed)
                    bFoundFreePort = true;
                }

                if(bFoundFreePort)
                   MSLSpecific.Logger.Info("Port:{0} free to use", Port);
                else
                   MSLSpecific.Logger.Info("Port:{0} not free to use", Port);
            }

            // Yeah, we have a very likely way that our configuration that should 
            // work 99% of the time without issue
            bool bWeCanUseThisConfig = false;
            if (bCheckPort && bFetchedExternalIP && bFoundFreePort)
                bWeCanUseThisConfig = true;
            else if (!bCheckPort && bFetchedExternalIP)
                bWeCanUseThisConfig = true;
            if (bWeCanUseThisConfig)
                MSLSpecific.Logger.Info("Found valid External IP:{0} and Port:{1} to use", ExternalIP, Port);
            else
                MSLSpecific.Logger.Error("Haven't found a valid External IP:{0} and Port:{1} to use", ExternalIP, Port);
            return bWeCanUseThisConfig;
        }

        /// <summary>
        /// Retrieve a Random Port Number * Auto Generated and retrieved/stored in Registry *
        /// </summary>
        /// <returns></returns>
        private int GetRandomOrSavedPortNumber(bool bForceRandomNumberCreation)
        {
            int SavedPort = 0;
            if (!bForceRandomNumberCreation)
                SavedPort = Yaulw.Registry.RegKey.GetKey<int>(Yaulw.Registry.HKEYRoot.LocalMachine, LOCAL_MACHINE_SUBKEY, LOCAL_MACHINE_EXTERNALPORT, 0);

            if (SavedPort <= 0 || SavedPort < EXTERNAL_MINIMUM_PORT_NUMBER || SavedPort > EXTERNAL_MAXIMUM_PORT_NUMBER)
            {
                Random random = new Random((int)DateTime.Now.Ticks);
                int nRand = random.Next(EXTERNAL_MINIMUM_PORT_NUMBER, EXTERNAL_MAXIMUM_PORT_NUMBER);
                SavedPort = random.Next(EXTERNAL_MINIMUM_PORT_NUMBER, EXTERNAL_MAXIMUM_PORT_NUMBER);
                Yaulw.Registry.RegKey.SetKey<int>(Yaulw.Registry.HKEYRoot.LocalMachine, LOCAL_MACHINE_SUBKEY, LOCAL_MACHINE_EXTERNALPORT, SavedPort);
            }
            return SavedPort;
        }

        #endregion

        #region Private RO Channel Helpers

        /// <summary>
        /// Activate all RO Channels
        /// </summary>
        private void ActivateROChannelsIfTheyHaveNotBeenActivated()
        {
            ActivateROInternalChannelIfHasNotBeenActivated();
            ActivateROExternalChannelIfHasNotBeenActivated();
        }

        /// <summary>
        /// Deactivate all RO Channels
        /// </summary>
        private void DeactivateROChannelsIfTheyHaveBeenActivated(bool bNetworkOutage)
        {
            DeactivateROInternalChannelIfItHasBeenActivated(bNetworkOutage);
            DeactivateROExternalChannelIfItHasBeenActivated(bNetworkOutage);
        }

        #endregion

        #region Private RO Internal Channel Helper

        /// <summary>
        /// Activate Internal
        /// </summary>
        private void ActivateROInternalChannelIfHasNotBeenActivated()
        {
            if (!ROInternalHasBeenActivated)
            {
                SetInternalPort(INTERNAL_CHANNEL_PORT);
                MSLTcpServerChannelInternal.Activate();
                ROInternalHasBeenActivated = true;
                AppLogInfo("MSLMobile Api Service Internal Channel Activated.");
                NetworkOutageDetected = false;
            }
        }

        /// <summary>
        /// Deactivate Internal
        /// </summary>
        private void DeactivateROInternalChannelIfItHasBeenActivated(bool bNetworkOutage)
        {            
            if (ROInternalHasBeenActivated)
            {
                MSLTcpServerChannelInternal.Deactivate();
                ROInternalHasBeenActivated = false;
                AppLogInfo("MSLMobile Api Service Internal Channel Deactivated.");
                NetworkOutageDetected = bNetworkOutage;
            }
        }

        #endregion

        #region Private RO External Channel Helper

        /// <summary>
        /// Activate External
        /// </summary>
        private void ActivateROExternalChannelIfHasNotBeenActivated()
        {
            if (!ROExternalHasBeenActivated && InitialExternalIPConfigurationIsSet)
            {
                SetExternalPort((int)ExternalPort);
                MSLTcpServerChannelExternal.Activate();
                ROExternalHasBeenActivated = true;
                AppLogInfo(String.Format("MSLMobile Api Service External Channel on Port:{0} Activated.", ExternalPort));
                NetworkOutageDetected = false;
                StartThePortMapper();
            }
        }

        /// <summary>
        /// Deactivate External
        /// </summary>
        private void DeactivateROExternalChannelIfItHasBeenActivated(bool bNetworkOutage)
        {            
            if (ROExternalHasBeenActivated)
            {
                StopThePortMapper();
                MSLTcpServerChannelExternal.Deactivate();
                ROExternalHasBeenActivated = false;
                AppLogInfo(String.Format("MSLMobile Api Service External Channel on Port:{0} Deactivated.", ExternalPort));
                NetworkOutageDetected = bNetworkOutage;
            }
        }

        #endregion

        #region Port Mapper

        /// <summary>
        /// Launch Port Mapper if not already running
        /// </summary>                
        private void StartThePortMapper()
        {
#if DEBUG
            if (!IsPortMapperRunning)
#else
            if (!IsOnMcKessonNetwork && !IsPortMapperRunning)
#endif
            {
                try
                {
                    AppLogInfo("Trying to start PortMapper");
                    Guid hostGuid = MSLSpecific.GetHostGUID();

                    // We have already done the port calculation,
                    // just pass in one port, that should be mapped * always *
                    //string minPort = ExternalPort.ToString();
                    //string maxPort = ExternalPort.ToString();
                    string externalPortToMap = ExternalPort.ToString();
                    string portMapDescription = "MSLMobile-" + ExternalPort.ToString();

                    // Call PORT FORWARDING Start method passing in the guid                              
                    BindPortMapperEvent();
                    //pMapper.Start(hostGuid.ToString(), minPort, maxPort, portMapDescription, LOCAL_MACHINE_SUBKEY, MSLSpecific.Logger);
                    pMapper.Start(ExternalIP, externalPortToMap, portMapDescription, MSLSpecific.Logger);
                    AppLogInfo("PortMapper Started.");
                    IsPortMapperRunning = true;
                }
                catch (Exception ex)
                {
                    AppLogError(String.Format("Error when attempting to start PortMapper: {0}. PortMapper Stopped.", ex.Message));
                    UnBindPortMapperEvent();
                    pMapper.Stop();
                }
            }
        }

        /// <summary>
        /// Stop Port Mapper if running
        /// </summary>
        private void StopThePortMapper()
        {
            // Stop the Port Mapper, if for any reason it started
            AppLogInfo("PortMapper stopping.");
            if (IsPortMapperRunning)
            {
                AppLogInfo("PortMapper stopped.");
                UnBindPortMapperEvent();
                pMapper.Stop();
            }
        }

        #endregion

        #region Port Mapper Event Bindings

        private void BindPortMapperEvent()
        {
            pMapper.DidFinishSearchForRouter += new PortMapper.PMDidFinishSearchForRouter(pMapper_DidFinishSearchForRouter);
            pMapper.ExternalIPAddressDidChange += new PortMapper.PMExternalIPAddressDidChange(pMapper_ExternalIPAddressDidChange);
            pMapper.CanAddPortMapping += new PortMapper.PMCanAddPortMapping(pMapper_CanAddPortMapping);
        }

        private void UnBindPortMapperEvent()
        {
            pMapper.DidFinishSearchForRouter -= new PortMapper.PMDidFinishSearchForRouter(pMapper_DidFinishSearchForRouter);
            pMapper.ExternalIPAddressDidChange -= new PortMapper.PMExternalIPAddressDidChange(pMapper_ExternalIPAddressDidChange);
            pMapper.CanAddPortMapping -= new PortMapper.PMCanAddPortMapping(pMapper_CanAddPortMapping);
        }

        #endregion

        #region Port Mapper Event Handlers

        void pMapper_DidFinishSearchForRouter(PortMapper sender)
        {
            AppLogInfo("MSLMobile Api Add port mapping.");
            pMapper.AddPortMapping();
        }

        void pMapper_ExternalIPAddressDidChange(PortMapper sender, IPAddress ip)
        {
            if (Yaulw.Net.IPHostHelper.IsValidIPv4Address(ip, false))
            {
                IPAddress extIP = IPAddress.None;
                bool bChangeOccured = Yaulw.Net.IPHostHelper.AreIPAddressesDifferent(ExternalIP, ip, out extIP);
                if (bChangeOccured)
                {
                    // We probably should call McKesson's Mobile Gateway?
                    // IF the router actually sends us the external IP! then yes, otherwise, to be discussed                
                    ExternalIP = extIP;
                    RegistrationWrapper.UpdateServerIntExt(InternalIP, ExternalIP, ExternalPort);                    
                    //Let's log this for now
                    MSLSpecific.Logger.Info("PMapper_ExternalIPAddress Changed Occured OldIP:{0} NewIP:{1}", ExternalIP, extIP);
                }
            }
        }

        void pMapper_CanAddPortMapping(PortMapper sender)
        {
            //This is fired after an external ip address change is detected and resolved
            MSLSpecific.Logger.Info("PMapper_CanAddPortMapping Occured Port:{0} ExternalIP:{1}", sender.ExternalPortToMap, sender.ExternalIPAddress);
            pMapper.AddPortMapping();
        }

        #endregion

        // usefull for debugging Service in visual studio
#if DEBUG
        internal void DebugStop() { OnStop(); }
#endif

        #region OnStop

        ///// <summary>
        ///// Stop Now (let's try this)
        ///// </summary>
        internal static void StopNow()
        {
            s_PlutoService.Stop();   
        }

        /// <summary>
        /// SCM OnStop() call-in
        /// </summary>
        protected override void OnStop()
        {
            // Stop the Connectivity timer
            try
            {
                if (connectivityTimer != null)
                {
                    connectivityTimer.Stop();
                    connectivityTimer = null;
                    AppLogInfo("End Connectivity Timer stopped.");
                }
            }
            catch (Exception) { /* ignore */ }

            // Deactivate RO Channels
            try
            {
                DeactivateROChannelsIfTheyHaveBeenActivated(false);
                AppLogInfo("End DeactivateROChannelsIfTheyHaveBeenActivated.");
            }
            catch (Exception) { /* ignore */ }

#if !DEBUG
            // Disable Auto-Update Timer
            try
            {                
                AutoUpdate.StopAutoTimer_ServiceStopped();
                AppLogInfo("End Auto-Update Timer stopped.");
            }
            catch (Exception) { /* ignore */ }
#endif
        }

        /// <summary>
        /// Force close this process (to make sure we are dead),
        /// we can't have open handles remaining open due to auto-update
        /// However, doing this means an error gets thrown in the SCM when the user manually
        /// stops the service!, so thie bool let's us know that it is us who is trying to do it.
        /// Last Resort, Try to kill the process to make sure the whole service is down. 
        /// </summary>
        internal static void KillCurrentProccess()
        {            
            try
            {
                var p = System.Diagnostics.Process.GetCurrentProcess();
                if (p != null)
                    p.Kill();                    
            }
            catch (Exception) { /* ignore */ }
        }

        #endregion
    }
}
