//WARNING: Do not update RestSharp beyond 106.x due to dependency on SqlWeb.Data and RestClient.ClearHandlers().

using System;
using System.Data.Common;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Threading;
using NHibernate;
using SqlWeb.Data;
using NHConfig = NHibernate.Cfg.Configuration;
using NHEnv = NHibernate.Cfg.Environment;

namespace WeatherPrep
{
    /// <summary>
    /// Handles creation and management of NHibernate sessions for accessing the i-Tree LocationSpecies database.
    /// </summary>
    public class SessionHelper : IDisposable
    {
        private static SessionHelper _instance;
        private ISessionFactory _sf;
        private bool _disposed;

        private SessionHelper()
        {
            _disposed = false;
            _sf = null;
        }

        /// <summary>
        /// Retrieves or creates the singleton instance of SessionHelper.
        /// </summary>
        public static SessionHelper GetInstance()
        {
            var instance = _instance;

            if (instance == null)
            {
                instance = new SessionHelper();
                instance = Interlocked.CompareExchange(ref _instance, instance, null);

                if (instance == null)
                {
                    _instance.CreateSessionFactory();
                }
            }

            return _instance;
        }

        /// <summary>
        /// NHibernate session factory for accessing the i-Tree LocationSpecies online database.
        /// </summary>
        public ISessionFactory SessionFactory => _sf;

        /// <summary>
        /// Opens a new NHibernate session connected to the i-Tree database.
        /// </summary>
        public static ISession OpenSession()
        {
            return GetInstance().SessionFactory.OpenSession();
        }

        /// <summary>
        /// Builds the NHibernate session factory using the embedded configuration file and cached config if available.
        /// </summary>
        private void CreateSessionFactory()
        {
            if (_sf == null || _sf.IsClosed)
            {
                Assembly asmLS = typeof(LocationSpecies.Domain.Location).Assembly;
                NHConfig config = GetNHConfig(asmLS);

                if (config == null)
                {
                    Assembly asm = Assembly.GetExecutingAssembly();
                    config = new NHConfig().Configure(asm, "WeatherPrep.LocationSpecies.cfg.xml");

                    //#if DEBUG_SQL will be active when building in Debug mode
                    //Note: Shows the actual SQL NHibernate sends to the i-Tree API or database in your output window (or log)
#if DEBUG_SQL
                    config.Properties[NHEnv.ShowSql] = "true";
#endif
                    //
                    UpdateNHConfig(asmLS, config);
                }

                VerifyDatabaseVersion(config, asmLS);
                _sf = config.BuildSessionFactory();
                ValidateCache(config);
            }
        }

        // ----------------------------
        // Configuration File Handling
        // ----------------------------

        private string GetConfigFile(Assembly lib)
        {
            FileVersionInfo vNH = FileVersionInfo.GetVersionInfo(typeof(NHConfig).Assembly.Location);
            string cfgDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "i-Tree", "WeatherPrep");
            string cfgFile = $"{lib.GetName().Name}.{vNH.FileVersion}.cfg";

            return Path.Combine(cfgDir, cfgFile);
        }

        private bool ConfigValid(string cfgFile, Assembly lib)
        {
            if (!File.Exists(cfgFile)) return false;

            FileInfo cfgInfo = new FileInfo(cfgFile);
            FileInfo libInfo = new FileInfo(lib.Location);
            return libInfo.LastWriteTime == cfgInfo.LastWriteTime;
        }

        private NHConfig GetNHConfig(Assembly lib)
        {
            string cfgFile = GetConfigFile(lib);

            if (!ConfigValid(cfgFile, lib)) return null;

            try
            {
                using (FileStream fs = new FileStream(cfgFile, FileMode.Open))
                {
                    BinaryFormatter bf = new BinaryFormatter();
                    return bf.Deserialize(fs) as NHConfig;
                }
            }
            catch (SerializationException)
            {
                return null;
            }
        }

        private void UpdateNHConfig(Assembly lib, NHConfig config)
        {
            string cfgFile = GetConfigFile(lib);
            string cfgDir = Path.GetDirectoryName(cfgFile);

            if (!Directory.Exists(cfgDir))
            {
                Directory.CreateDirectory(cfgDir);
            }

            using (FileStream fs = new FileStream(cfgFile, FileMode.Create))
            {
                BinaryFormatter bf = new BinaryFormatter();
                bf.Serialize(fs, config);
            }

            FileInfo libInfo = new FileInfo(lib.Location);
            File.SetLastWriteTime(cfgFile, libInfo.LastWriteTime);
        }

        // ----------------------------
        // Database Version Checking
        // ----------------------------

        private void VerifyDatabaseVersion(NHConfig config, Assembly assembly)
        {
            Version dbVersion = GetDatabaseVersion(config);
            Version domainVersion = GetAssemblyProductVersion(assembly);

            if (dbVersion.Major != domainVersion.Major)
            {
                throw new Exception("i-Tree Data version is incompatible with domain version.");
            }
        }

        /// <summary>
        /// Queries the i-Tree LocationSpecies database to obtain its version number.
        /// </summary>
        private Version GetDatabaseVersion(NHConfig config)
        {
            string connectionString = config.Properties[NHEnv.ConnectionString];

            // Packages "RestSharp" and "RestSharp.Serializers.NewtonsoftJson" cannot currently be updated beyond version 106.12.
            // RestSharp 106.x, which is compatible with the SqlWeb.Data.dll used in WeatherPrep
            // RestSharp 106.x depends on RestClient.ClearHandlers(), which was removed in RestSharp 107+ 
            using (DbConnection con = new SqlWebConnection(connectionString))
            using (DbCommand cmd = new SqlWebCommand())
            {
                con.Open();

                cmd.Connection = con;
                cmd.CommandText = "SELECT r.Value FROM _DatabaseRegistry r WHERE RegistryId = 4";

                object value = cmd.ExecuteScalar();
                return value != null && value != DBNull.Value
                    ? new Version(Convert.ToString(value))
                    : null;
            }
        }

        private Version GetAssemblyProductVersion(Assembly assembly)
        {
            FileVersionInfo versionInfo = FileVersionInfo.GetVersionInfo(assembly.Location);
            return new Version(versionInfo.ProductVersion);
        }

        // ----------------------------
        // Cache Validation
        // ----------------------------

        private void ValidateCache(NHConfig config)
        {
            Version cacheVersion = GetCacheVersion();
            Version dbVersion = GetDatabaseVersion(config);

            if (dbVersion != cacheVersion)
            {
                ClearCache();
                GetCacheVersion(); // force cache rebuild
            }
        }

        private Version GetCacheVersion()
        {
            Version version = null;

            using (ISession session = _sf.OpenSession())
            using (ITransaction tx = session.BeginTransaction())
            {
                var entry = session.Get<LocationSpecies.Domain.RegistryEntry>(4);
                if (!string.IsNullOrEmpty(entry?.Value))
                {
                    version = new Version(entry.Value);
                }

                tx.Commit();
            }

            return version;
        }

        private void ClearCache()
        {
            foreach (var entity in _sf.GetAllClassMetadata().Keys)
            {
                _sf.EvictEntity(entity);
            }

            foreach (var collection in _sf.GetAllCollectionMetadata().Keys)
            {
                _sf.EvictCollection(collection);
            }

            _sf.EvictQueries();
        }

        // ----------------------------
        // Disposal
        // ----------------------------

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        private void Dispose(bool disposing)
        {
            if (_disposed) return;

            if (disposing && _sf != null && !_sf.IsClosed)
            {
                _sf.Dispose();
                _sf = null;
            }

            Interlocked.CompareExchange(ref _instance, null, this);
            _disposed = true;
        }
    }
}
