using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Abstractions;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using BO.ProcessServer.Business.StatisticCounters;
using BO.ProcessServer.Business.Workers;
using BO.ProcessServer.Business.Workers.Contexts;
using BO.ProcessServer.Options;
using Connector.PS;
using Quadarax.Foundation.Core.IO;
using Quadarax.Foundation.Core.Logging;
using Quadarax.Foundation.Core.Object;

namespace BO.ProcessServer.Business
{
    internal class Server : DisposableObject
    {
        #region *** Private fields ***
        private Configuration _configuration;
        private IFileSystem _fileSystem;
        private IList<ServerProcess> _processes;
        protected ILog Log { get; }
        protected ILog LogLifecycle { get; }
        protected Statistics Statistics { get; }
        private Pool _pool;
        private PullDocumentsWorker _wkPull;
        private RetentionWorker _wkRetention;
        private ProcessingDocumentsWorker _wkProcessing;
        private IConnectionPS _connection;
        #endregion

        #region *** Public properties ***
        public bool IsRunning { get; private set; }
        #endregion

        #region *** Constructors ***
        public Server(Configuration serverConfiguration, IFileSystem fileSystem, ILogger logger)
        {
            _configuration = serverConfiguration ?? throw new ArgumentNullException(nameof(serverConfiguration));
            _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));


            if (logger == null)
                throw new ArgumentNullException(nameof(logger));
            Log = logger.GetLogger(typeof(Server));
            LogLifecycle = logger.GetLogger("Lifecycle");
            Statistics = new Statistics();

            ValidateConfiguration();
            
            _processes = new List<ServerProcess>();

            _connection = new Connector(_configuration, _fileSystem);

            _pool = new Pool(_configuration, _fileSystem, logger);

            _wkPull = new PullDocumentsWorker(_connection, _configuration.System.PoolPullInterval, _fileSystem, Statistics, _pool, logger);
            _wkRetention = new RetentionWorker(new CfgCtx(_configuration, Statistics, _pool));
            _wkProcessing = new ProcessingDocumentsWorker(_configuration, _processes, _connection, _fileSystem, Statistics, _pool, logger);
        }
        #endregion

        #region *** Public operations ***
        public void Start()
        {
            if (IsRunning)
            {
                LogWarn($"Instance '{_configuration.System.Identification}' already running. Start skipped.");
                return;
            }

            LogDebug($"Starting instance '{_configuration.System.Identification}' ...");

            _wkPull.Start();
            if (_configuration.System.Retention.Enabled)
                _wkRetention.Start();
            else
                LogWarn("Retention module is disabled by configuration.");
            _wkProcessing.Start();

            Statistics.GetCounter<TimespanCounter>(Statistics.CNT_PS_ONLINE_DUR).Start();
            IsRunning = true;
            LogInfo( $"Instance '{_configuration.System.Identification}' STARTED.");
            LogLifecycle.Log(LogSeverityEnum.Info, $"Instance '{_configuration.System.Identification}' STARTED.");
        }


        public void Stop()
        {
            if (!IsRunning)
            {
                LogWarn($"Instance '{_configuration.System.Identification}' already stopped. Stop skipped.");
                return;
            }
            LogDebug($"Stopping instance '{_configuration.System.Identification}' ...");

            StopAll();

            Statistics.GetCounter<TimespanCounter>(Statistics.CNT_PS_ONLINE_DUR).Stop();
            IsRunning = false;
            LogInfo( $"Instance '{_configuration.System.Identification}' STOPPED.");
            LogLifecycle.Log(LogSeverityEnum.Info, $"Instance '{_configuration.System.Identification}' STOPPED.");
            LogLifecycle.Log(LogSeverityEnum.Info, Statistics.ToString());
        }
        #endregion

        #region *** Overrides ***
        protected override void OnDisposing()
        {
            if (IsRunning)
                StopAll(true);
            _wkPull?.Dispose();
            _wkRetention?.Dispose();
            _wkProcessing?.Dispose();
            _connection?.Dispose();
            _pool?.Dispose();

            LogDebug($"Instance '{_configuration.System.Identification}' disposed.");
            _configuration = null;
            _fileSystem = null;
            _wkPull=null;
            _wkRetention=null;
            _wkProcessing = null;
            _connection=null;
            _pool = null;
        }
        #endregion

        #region *** Private Operations ***

        public void LogDebug(string message)
        {
            LogRaw(LogSeverityEnum.Debug, message);
        }
        public void LogInfo(string message)
        {
            LogRaw(LogSeverityEnum.Info, message);
        }
        public void LogWarn(string message)
        {
            LogRaw(LogSeverityEnum.Warn, message);
        }

        public void LogRaw(LogSeverityEnum severity, string message)
        {
            Log.Log(severity, message);
        }

        private void ValidateConfiguration()
        {
            if (string.IsNullOrEmpty(_configuration.System.Identification))
                throw new ArgumentNullException(nameof(_configuration.System.Identification),
                    "Mandatory process server identification.");

            if (string.IsNullOrEmpty(_configuration.System.PreferredInterface))
                _configuration.System.PreferredInterface = "auto";

            
            var host = Dns.GetHostEntry(Dns.GetHostName());
            if (_configuration.System.PreferredInterface.Trim().ToLower() == "auto")
            {
                _configuration.System.PreferredInterface = host.AddressList.Where(x =>
                        x.AddressFamily == AddressFamily.InterNetwork ||
                        x.AddressFamily == AddressFamily.InterNetworkV6).OrderBy(x=>x.AddressFamily).FirstOrDefault()
                    ?.ToString();
            }

            if (!host.AddressList.Any(x =>
                    (x.AddressFamily == AddressFamily.InterNetwork ||
                     x.AddressFamily == AddressFamily.InterNetworkV6) && x.ToString().ToLower() ==
                    _configuration.System.PreferredInterface.Trim().ToLower()))
                throw new ArgumentNullException(nameof(_configuration.System.PreferredInterface),
                    "Mandatory preferred interface (or auto) for server identification.");

            // Pool directories structure
            EnsureDirectory(_configuration.System.PoolPathBase);
            EnsureDirectory(_fileSystem.Path.Combine(_configuration.System.PoolPathBase, _configuration.System.PoolPathDone));
            EnsureDirectory(_fileSystem.Path.Combine(_configuration.System.PoolPathBase, _configuration.System.PoolPathFail));
            EnsureDirectory(_fileSystem.Path.Combine(_configuration.System.PoolPathBase, _configuration.System.PoolPathPending));
            EnsureDirectory(_fileSystem.Path.Combine(_configuration.System.PoolPathBase, _configuration.System.PoolPathWorking));

            // Scenario structure
            EnsureDirectory(_configuration.System.ScenarioPathBase);
            if (_fileSystem.Directory.GetFiles(TranslatePath(_configuration.System.ScenarioPathBase), "*.*").Length == 0)
                LogWarn($"Scenario directory '{TranslatePath(_configuration.System.ScenarioPathBase)}' is empty.");

            LogInfo("Configuration validated.");
        }

        private string EnsureDirectory(string path)
        {
            var dir = FileUtils.EnsurePathEndsWithDirSeparator(_fileSystem, TranslatePath(path));
            if (!_fileSystem.Directory.Exists(dir))
            {
                _fileSystem.Directory.CreateDirectory(dir);
                LogDebug($"Directory '{dir}' created.");
            }

            if (!_fileSystem.Directory.Exists(dir))
                throw new FileNotFoundException($"Directory '{dir}' not found!");
            return dir;
        }

        private string TranslatePath(string path)
        {
            if (string.IsNullOrEmpty(path))
                return path;
            return path.Replace("{CD}", _fileSystem.Directory.GetCurrentDirectory());
        }

        private void StopAll(bool silently = false)
        {
            _wkPull.Stop(silently);
            if (_configuration.System.Retention.Enabled)
                _wkRetention.Stop(silently);
            _wkProcessing.Stop(silently);

            foreach (var process in _processes)
                process.Stop(silently);

            foreach (var process in _processes)
                process.Dispose();

            _processes.Clear();
        }
        #endregion
    }
}
