//
// Initializer.cs
//
// Author:
//       Carsten Sonne Larsen <cs@innolan.dk>
//
// Copyright (c) 2013-2016 Carsten Sonne Larsen
//
// 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.Graph;
using Ntp.Analyzer.Config.Page;
using Ntp.Analyzer.Config.Root;
using Ntp.Analyzer.Data;
using Ntp.Analyzer.Export;
using Ntp.Analyzer.Import;
using Ntp.Analyzer.Localize;
using Ntp.Analyzer.Objects;
using Ntp.Analyzer.Process.Description;
using Ntp.Common;
using Ntp.Data;
using Ntp.Data.Provider;
using Ntp.Interop;
using Ntp.Log;
using Ntp.Monitor.Client;
using Ntp.Monitor.Server;
using Ntp.Process;

namespace Ntp.Analyzer.Process
{
    public sealed class Initializer
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="Ntp.Analyzer.Process.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 = VersionInfo.Number;
        }

        private static bool firstrun = true;
        private readonly string configFile;
        private readonly LogGroup initlog;
        private readonly List<Listener> listeners;
        private readonly string name;
        private readonly List<IRequest> nodes;
        private readonly int pid;
        private readonly string pidFile;
        private readonly string version;
        private Configuration config;
        private SignalHandler controller;
        private LogBase log;
        private bool ready;
        private Scheduler scheduler;

        public SignalHandler Controller
        {
            get { return controller; }
        }

        public Scheduler Scheduler
        {
            get { return scheduler; }
        }

        public LogBase Log
        {
            get { return log; }
        }

        public List<IRequest> Nodes
        {
            get { return nodes; }
        }

        public List<Listener> Listeners
        {
            get { return listeners; }
        }

        public bool Ready
        {
            get { return ready; }
        }

        /// <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 configuration.
        /// </summary>
        private bool InitializeConfiguration()
        {
            if (!File.Exists(configFile))
            {
                initlog.WriteLine(String.Format(
                    InitializationMessage.NoConfig, configFile),
                    Severity.Error);

                return false;
            }

            var reader = new ConfigurationReader(configFile);
            if (!reader.Execute())
            {
                foreach (string error in reader.Errors)
                {
                    initlog.WriteLine(error, Severity.Error);
                }
                return false;
            }

            config = reader.Configuration;
            return true;
        }

        /// <summary>
        /// Initializes the log.
        /// </summary>
        private bool InitializeLog()
        {
            // Create log.
            try
            {
                log = LogFactory.CreateLog(config.Log);
                log.Initialize();
            }
            catch (Exception ex)
            {
                initlog.WriteLine(String.Format(
                    InitializationMessage.NoConfig, ex.Message),
                    Severity.Error);

                return false;
            }

            initlog.Add(log);

            // Initialize log.
            if (firstrun)
            {
                initlog.WriteLine(String.Format(
                    InitializationMessage.Welcome, version), Severity.Notice);
            }

            initlog.WriteLine(String.Format(
                InitializationMessage.UsingConfig, configFile), Severity.Notice);

            initlog.WriteLine(String.Format(
                InitializationMessage.UsingPid, pid), Severity.Notice);

            initlog.WriteLine(String.Format(
                InitializationMessage.UsingName, name), Severity.Notice);

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

            if (Permission.SetUserId(config.Permission.AppUserId.Value))
            {
                initlog.WriteLine(String.Format(
                    InitializationMessage.UsingUserId, config.Permission.AppUserId.Value),
                    Severity.Info);
            }

            #if !GRAPH
            foreach (HostConfiguration server in config.Servers)
            {
                if (server.HostGraphs.Count() != 0 ||
                    server.PeerGraphs.Count() != 0 ||
                    server.TrafficGraphs.Count() != 0)
                {
                    initlog.WriteLine(InitializationMessage.NoGraphBuild, Severity.Warn);
                    break;
                }
            }
            #endif

            return true;
        }

        /// <summary>
        /// Initializes the security settings.
        /// </summary>
        private bool InitializeSecurity()
        {
            // Drop privileges
            if (config.Permission != null && config.Permission.AppUserId.HasValue)
            {
                if (!Permission.SetUserId(config.Permission.AppUserId.Value))
                {
                    initlog.WriteLine(String.Format(
                        InitializationMessage.UserIdError, config.Permission.AppUserId.Value),
                        Severity.Warn);
                }
            }

            // Create pid file
            if (pidFile != null)
            {
                try
                {
                    File.WriteAllText(pidFile, pid.ToString(CultureInfo.InvariantCulture));
                }
                catch (Exception e)
                {
                    initlog.WriteLine(String.Format(
                        InitializationMessage.PidFileError, e.Message),
                        Severity.Warn);

                    return false;
                }
            }

            // Set permissions
            if (config.Permission != null)
            {
                FileSystemDestination.FileMask = config.Permission.FileMode;

                FileSystemDestination.FileUserId =
                    config.Permission != null
                        ? config.Permission.UserId
                        : null;

                FileSystemDestination.FileGroupId =
                    config.Permission != null
                        ? config.Permission.GroupId
                        : null;
            }

            return true;
        }

        /// <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)
            {
                var beat = new Heartbeat(log, config.Heartbeat.Rate);
                beat.Start();
            }
        }

        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;

            // Initialize database schema
            if (config.Database.Initialize)
            {
                try
                {
                    var initializer = new DatabaseInitializer(config.Database.Upgrade, log);
                    initializer.Execute();
                }
                catch (Exception e)
                {
                    initlog.WriteLine(InitializationMessage.DatabaseError, Severity.Error);
                    initlog.WriteLine(e.Message, Severity.Debug);
                    initlog.WriteLine(e, Severity.Trace);
                }
            }

            return true;
        }

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

            try
            {
                // Initialize hosts
                foreach (HostConfiguration server in config.Servers)
                {
                    Host 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;
                        }
                        host = new Host(server.HostId, server.ServerName, ip.ToString(), null);
                        DataFace.Instance.Hosts.Save(host);
                        Log.WriteLine(String.Format(
                            InitializationMessage.NewHost, server.HostId, server.ServerName, ip),
                            Severity.Info);
                    }

                    // Initialize peers
                    Importer<AssociationEntry> peerImporter = ImportFactory.CreatePeerImporter(
                        server.ServerName, server.ServerType, host, log);

                    foreach (AssociationEntry entry in peerImporter.ToList())
                    {
                        AssociationEntry entry1 = entry;
                        IEnumerable<Peer> peerList = DataFace.Instance.Peers.Where(p => p.Ip == entry1.Remote);
                        if (peerList.Any())
                            continue;

                        var peer = new Peer(entry.Remote, entry.Remote, null);
                        DataFace.Instance.Peers.Save(peer);
                        Log.WriteLine(String.Format(
                            InitializationMessage.NewPeer, entry.Remote, entry.Remote),
                            Severity.Info);
                    }
                }
            }
            catch (Exception e)
            {
                log.WriteLine(InitializationMessage.TableError, Severity.Error);
                log.WriteLine(e.Message, Severity.Debug);
                log.WriteLine(e, Severity.Trace);
            }
        }

        /// <summary>
        /// Initializes the listerners.
        /// </summary>
        private void InitializeListerners()
        {
            try
            {
                foreach (ListenerConfiguration monitor in config.Monitors)
                {
                    var listener = new Listener(monitor.Ip, monitor.Port, log);
                    listener.Open();
                    listeners.Add(listener);

                    initlog.WriteLine(String.Format(
                        InitializationMessage.ListenerReady, listener), Severity.Notice);
                }
            }
            catch (Exception e)
            {
                initlog.WriteLine(InitializationMessage.ListenerError, Severity.Warn);
                initlog.WriteLine(e.Message, Severity.Debug);
                initlog.WriteLine(e, Severity.Trace);
            }
        }

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

            try
            {
                foreach (NodeConfiguration node in config.Cluster.Nodes)
                {
                    IPAddress ip = IPAddress.Parse(node.Ip);

                    IRequest req = new TextRequest(ip, node.Port);
                    nodes.Add(req);

                    log.WriteLine(String.Format(
                        InitializationMessage.ClusterReady, node.Address), Severity.Notice);
                }
            }
            catch (Exception e)
            {
                initlog.WriteLine(InitializationMessage.ClusterError, Severity.Error);
                initlog.WriteLine(e.Message, Severity.Debug);
                initlog.WriteLine(e, Severity.Trace);
            }
        }

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

            try
            {
                scheduler = new Scheduler(log);
                log.WriteLine(InitializationMessage.JobInitStart, Severity.Info);

                foreach (ReadingBulkConfiguration bulk in config.Bulks)
                    scheduler.Add(new BulkStatJob(bulk, scheduler.Log));

                // Add jobs to schedule.
                foreach (HostConfiguration 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 AboutPageJob(server.Menu, server.AboutPage, scheduler.Log));

                    foreach (PeerPageConfiguration peerPage in server.PeerPages)
                        scheduler.Add(new PeerPageJob(server.Menu, peerPage, scheduler.Log));

                    foreach (HostPageConfiguration hostPage in server.HostPages)
                        scheduler.Add(new HostPageJob(server.Menu, hostPage, scheduler.Log));

                    foreach (HostGraphConfiguration hostGraph in server.HostGraphs)
                        scheduler.Add(new HostGraphJob(hostGraph, scheduler.Log));

                    foreach (TrafficGraphConfiguration trafficGraph in server.TrafficGraphs)
                        scheduler.Add(new TrafficGraphJob(trafficGraph, scheduler.Log));

                    foreach (PeerGraphConfiguration peerGraph in server.PeerGraphs)
                        scheduler.Add(new PeerGraphJob(peerGraph, scheduler.Log));

                    foreach (PeerSummaryPageConfiguration 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);

                    // Done
                    log.WriteLine(InitializationMessage.JobInitEnd, Severity.Debug);
                }
            }
            catch (Exception e)
            {
                initlog.WriteLine(InitializationMessage.SchedulerError, Severity.Error);
                initlog.WriteLine(e.Message, Severity.Debug);
                initlog.WriteLine(e, Severity.Trace);
            }

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