// 
// Copyright (c) 2013-2016 Carsten Sonne Larsen <cs@innolan.dk>
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading;
using Ntp.Analyzer.Config;
using Ntp.Analyzer.Config.Node;
using Ntp.Analyzer.Data;
using Ntp.Analyzer.Data.Import;
using Ntp.Analyzer.Export;
using Ntp.Analyzer.Import;
using Ntp.Analyzer.Monitor.Client;
using Ntp.Analyzer.Monitor.Server;
using Ntp.Analyzer.Objects;
using Ntp.Analyzer.Process.Description;
using Ntp.Analyzer.Process.Log;
using Ntp.Common.App;
using Ntp.Common.IO;
using Ntp.Common.Log;
using Ntp.Common.Process;
using Ntp.Common.System;
using Ntp.Data;
using Ntp.Data.Provider;

namespace Ntp.Analyzer.Process
{
    public sealed class Initializer
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="Initializer" /> class.
        /// </summary>
        /// <param name="configFile">Config file to use.</param>
        /// <param name="pid">Process ID.</param>
        /// <param name="pidFile">Where to write Process ID file.</param>
        /// <param name="name">Name of process ID. Used when shutting down.</param>
        /// <param name="initlog">Log used during intitialization.</param>
        public Initializer(string configFile, int pid, string pidFile, string name, LogGroup initlog)
        {
            this.configFile = configFile;
            this.pid = pid;
            this.pidFile = pidFile;
            this.name = name;
            this.initlog = initlog;
            Nodes = new List<IRequest>();
            Listeners = new List<Listener>();
            version = "0.8.1";
        }

        private static bool firstrun = true;
        private readonly string configFile;
        private readonly LogGroup initlog;
        private readonly string name;
        private readonly int pid;
        private readonly string pidFile;
        private readonly string version;
        private Configuration config;

        public SignalHandler Controller { get; private set; }

        public Scheduler Scheduler { get; private set; }

        public LogBase Log { get; private set; }

        public List<IRequest> Nodes { get; }

        public List<Listener> Listeners { get; }

        public bool Ready { get; private set; }

        /// <summary>
        /// Run the NTP Analyzer.
        /// </summary>
        public void Run()
        {
            // Neutralize.
            Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;

            bool proceed = InitializeConfiguration();

            if (proceed)
                proceed = InitializeSecurity();

            if (proceed)
                proceed = InitializeLog();

            if (proceed)
            {
                InitializeApplication();
                InitializeListerners();
                InitializeCluster();
                InitializeScheduler();
                proceed = InitializeDatabase();
            }

            if (proceed)
                InitializeData();

            Ready = proceed;
            firstrun = false;
        }

        /// <summary>
        /// Initializes the application and database state.
        /// </summary>
        private void InitializeApplication()
        {
            // Start signal controller
            Controller = new SignalHandler(name, initlog);
            Controller.Start();

            // Initialize application state
            ApplicationState.Version = version;
            ApplicationState.Pid = pid;
            ApplicationState.Name = name;
            ApplicationState.Config = config;
            ApplicationState.ConfigFile = configFile;
            ApplicationState.Log = Log;

            if (config.Heartbeat == null)
                return;

            var beat = new Heartbeat(Log, config.Heartbeat.Rate);
            beat.Start();
        }

        /// <summary>
        /// Initializes the cluster nodes.
        /// </summary>
        private void InitializeCluster()
        {
            if (config.Cluster == null)
                return;

            try
            {
                foreach (var node in config.Cluster.Nodes)
                {
                    var ip = IPAddress.Parse(node.Ip);
                    var req = new TextRequest(ip, node.Port);
                    Nodes.Add(req);
                    Log.ClusterReady(node);
                }
            }
            catch (Exception e)
            {
                initlog.ClusterError(e);
            }
        }

        /// <summary>
        /// Initializes the configuration.
        /// </summary>
        private bool InitializeConfiguration()
        {
            if (!File.Exists(configFile))
            {
                initlog.NoConfig(configFile);
                return false;
            }

            var builder = new ConfigBuilder(configFile);
            config = builder.Execute();

            if (!builder.Errors.Any() && config != null)
                return true;

            initlog.AddErrors(builder.Errors);
            return false;
        }

        private void InitializeData()
        {
            if (!config.Database.Initialize)
                return;

            TimeServerWebAdapter.Initialize(config.Database.Import);

            try
            {
                // Initialize hosts
                foreach (var server in config.Servers)
                {
                    var host = DataFace.Instance.Hosts.SingleOrDefault(h => h.Id == server.HostId);
                    if (host == null)
                    {
                        IPAddress ip;
                        try
                        {
                            ip = Dns.GetHostAddresses(server.ServerName)[0];
                        }
                        catch
                        {
                            ip = IPAddress.None;
                        }

                        int? orgId = null;
                        if (TimeServers.List.ContainsKey(server.ServerName))
                        {
                            orgId = TimeServers.List[server.ServerName];
                            Log.KnownServer(orgId.Value, server.ServerName);
                        }
                        else if (!Equals(ip, IPAddress.None) && TimeServers.List.ContainsKey(ip.ToString()))
                        {
                            orgId = TimeServers.List[ip.ToString()];
                            Log.KnownServer(orgId.Value, ip.ToString());
                        }

                        host = new Host(server.HostId, server.ServerName, ip.ToString(), orgId);
                        DataFace.Instance.Hosts.Save(host);
                        Log.NewHost(server, ip);
                    }
                    else if (host.OrgId == null && TimeServers.List.ContainsKey(host.Ip))
                    {
                        host.OrgId = TimeServers.List[host.Ip];
                        Log.KnownServer(host.OrgId.Value, host.Ip);
                        DataFace.Instance.Hosts.Save(host);
                    }
                    else if (host.OrgId == null && TimeServers.List.ContainsKey(host.Name))
                    {
                        host.OrgId = TimeServers.List[host.Name];
                        Log.KnownServer(host.OrgId.Value, host.Name);
                        DataFace.Instance.Hosts.Save(host);
                    }

                    // Flush old association entries
                    DataFace.Instance.AssociationEntries.Delete(server.HostId);

                    // Initialize peers
                    var peerImporter = ImportFactory.CreatePeerImporter(server.ServerName, server.ServerType, host, Log);

                    foreach (var entry in peerImporter.ToList())
                    {
                        var currentEntry = entry;
                        var peerList = DataFace.Instance.Peers.Where(p => p.Ip == currentEntry.Remote).ToList();

                        if (!peerList.Any())
                        {
                            // Create new peer in database
                            TimeServer timeServer = null;
                            if (TimeServers.List.ContainsKey(entry.Remote))
                            {
                                int orgId = TimeServers.List[entry.Remote];
                                timeServer = DataFace.Instance.Servers[orgId];
                                Log.KnownServer(orgId, entry.Remote);
                            }

                            var hostName = entry.Remote;

                            try
                            {
                                var hostEntry = Dns.GetHostEntry(entry.Remote);
                                hostName = hostEntry.HostName;
                            }
                            catch (Exception e)
                            {
                                Log.HostNameNotFound(entry.Remote, e);
                            }

                            var peer = new Peer(hostName, entry.Remote, timeServer);
                            DataFace.Instance.Peers.Save(peer);
                            Log.NewPeer(entry);
                        }
                        else if (TimeServers.List.ContainsKey(entry.Remote))
                        {
                            // Try to update existing peer
                            var peer = peerList.First();
                            if (peer.Server != null && peer.Server.IsOrgServer)
                                continue;

                            int orgId = TimeServers.List[entry.Remote];
                            var timeServer = DataFace.Instance.Servers[orgId];
                            Log.KnownServer(orgId, entry.Remote);
                            if (timeServer == null)
                                continue;

                            peer.Server = timeServer;
                            DataFace.Instance.Peers.Save(peer);
                        }
                    }
                }
            }
            catch (Exception e)
            {
                Log.TableError(e);
            }
        }

        private bool InitializeDatabase()
        {
            SqlDatabaseFactory.Initialize(config.Database);
            DataFace.Initialize(Log);

            var checker = new SqlDatabaseChecker(SqlDatabaseFactory.Instance, Controller, initlog);
            checker.CheckConnection();

            if (Controller.Stopped)
                return false;

            try
            {
                var initializer = new DatabaseInitializer(
                    config.Database.Initialize,
                    config.Database.Upgrade,
                    Log);

                return initializer.Execute();
            }
            catch (Exception e)
            {
                initlog.DatabaseError(e);
            }

            return false;
        }

        /// <summary>
        /// Initializes the listerners.
        /// </summary>
        private void InitializeListerners()
        {
            try
            {
                foreach (var monitor in config.Monitors)
                {
                    var listener = new Listener(monitor.Ip, monitor.Port, Log);
                    listener.Open();
                    Listeners.Add(listener);
                    initlog.ListenerReady(listener);
                }
            }
            catch (Exception e)
            {
                initlog.ListenerError(e);
            }
        }

        /// <summary>
        /// Initializes the log.
        /// </summary>
        private bool InitializeLog()
        {
            try
            {
                Log = LogFactory.CreateLog(config.Log);
                Log.Initialize();
            }
            catch (Exception e)
            {
                initlog.InitializationError(e);
                return false;
            }

            initlog.Add(Log);

            if (firstrun)
                initlog.Starting(version);

            initlog.ConfigFile(configFile);
            initlog.ProcessId(pid);
            initlog.InstanceName(name);

            if (config.Permission?.AppUserId == null)
                return true;

            if (Permission.SetUserId(config.Permission.AppUserId.Value))
                initlog.UserId(config.Permission.AppUserId.Value);

            return true;
        }

        /// <summary>
        /// Initializes the scheduler.
        /// </summary>
        private void InitializeScheduler()
        {
            Job.Reset();

            try
            {
                Scheduler = new Scheduler(Log);
                Log.JobInitStart();

                foreach (var bulk in config.Bulks)
                    Scheduler.Add(new BulkStatJob(bulk, Scheduler.Log));

                // Add jobs to schedule.
                foreach (var server in config.Servers)
                {
                    Scheduler.Add(new HostStatJob(server.HostStats, Scheduler.Log));
                    Scheduler.Add(new HostIoStatJob(server.HostIoStats, Scheduler.Log));
                    Scheduler.Add(new PeerStatJob(server.PeerStats, Scheduler.Log));
                    Scheduler.Add(new DriftStatJob(server.DriftStats, Scheduler.Log));
                    Scheduler.Add(new AboutPageJob(server.Menu, server.AboutPage, Scheduler.Log));

                    foreach (var peerPage in server.PeerPages)
                        Scheduler.Add(new PeerPageJob(server.Menu, peerPage, Scheduler.Log));

                    foreach (var hostPage in server.HostPages)
                        Scheduler.Add(new HostPageJob(server.Menu, hostPage, Scheduler.Log));

                    foreach (var hostGraph in server.HostGraphs)
                        Scheduler.Add(new HostGraphJob(hostGraph, Scheduler.Log));

                    foreach (var trafficGraph in server.TrafficGraphs)
                        Scheduler.Add(new TrafficGraphJob(trafficGraph, Scheduler.Log));

                    foreach (var peerGraph in server.PeerGraphs)
                        Scheduler.Add(new PeerGraphJob(peerGraph, Scheduler.Log));

                    foreach (var peerSummaryPage in server.PeerSummaryPages)
                        Scheduler.Add(new PeerSummaryJob(server.Menu, peerSummaryPage, Scheduler.Log));

                    Scheduler.Add(new HostGraphPageJob(server.HostGraphPage, Scheduler.Log));
                    Scheduler.Add(new PeerGraphPageJob(server.PeerGraphPage, Scheduler.Log));
                }

                // Add the survaliance/notify job.
                if (config.Notify != null)
                {
                    var adminJob = new NotifyJob(config.Notify, Log, pid, configFile);
                    Scheduler.Add(adminJob);
                    adminJob.SetJobList(Scheduler);
                }

                Log.JobInitEnd();
            }
            catch (Exception e)
            {
                initlog.SchedulerError(e);
            }

            ApplicationState.Scheduler = Scheduler;
            ApplicationState.StartupTime = Scheduler.StartTime;
        }

        /// <summary>
        /// Initializes the security settings.
        /// </summary>
        private bool InitializeSecurity()
        {
            // Drop privileges
            if (config.Permission?.AppUserId != null)
            {
                if (!Permission.SetUserId(config.Permission.AppUserId.Value))
                {
                    initlog.UserIdError(config.Permission.AppUserId.Value);
                }
            }

            // Create pid file
            if (pidFile != null)
            {
                try
                {
                    File.WriteAllText(pidFile, pid.ToString(CultureInfo.InvariantCulture));
                }
                catch (Exception e)
                {
                    initlog.PidFileError(e);
                    return false;
                }
            }

            // Set permissions
            if (config.Permission == null)
                return true;

            FileSystemDestination.FileMask = config.Permission.FileMode;
            FileSystemDestination.FileUserId = config.Permission?.UserId;
            FileSystemDestination.FileGroupId = config.Permission?.GroupId;

            return true;
        }
    }
}