using System;
using System.Collections.Generic;
using System.Linq;
using VCommon.Collections;
using VCommon.Linq;

namespace VCommon.VApplication.Setting
{
    public abstract class SettingManagerBase : ISettingManager
    {
        private readonly IVSession _session;
        private readonly IReadOnlyDictionary<string, SettingDefinition> _definitions;

        private readonly ReadOnlyHashSet<string> _applicationScopeNames;
        private readonly ReadOnlyHashSet<string> _tenantScopeNames;
        private readonly ReadOnlyHashSet<string> _userScopeNames;
        private readonly ReadOnlyHashSet<string> _scriptWriteableNames;

        private readonly IReadOnlyCollection<string> _scriptOutputNames;
        private readonly IReadOnlyCollection<string> _applicationScriptOutputNames;

        protected SettingManagerBase(IVSession session)
        {
            _session = session;
            // ReSharper disable once VirtualMemberCallInConstructor
            _definitions = Init();

            var settingDefinitions = _definitions.Values.ToArray();

            _applicationScopeNames = settingDefinitions.Where(p => p.Scope == SettingScope.Application).Select(p => p.Name).ToHashSet();
            _tenantScopeNames = settingDefinitions.Where(p => p.Scope == SettingScope.Tenant).Select(p => p.Name).ToHashSet();
            _userScopeNames = settingDefinitions.Where(p => p.Scope == SettingScope.User).Select(p => p.Name).ToHashSet();
            _scriptOutputNames = settingDefinitions.Where(p => p.ScriptAccess != SettingScriptAccess.None).Select(p => p.Name).ToArray();
            _scriptWriteableNames = settingDefinitions.Where(p => p.ScriptAccess == SettingScriptAccess.ReadWrite).Select(p => p.Name).ToHashSet();
            _applicationScriptOutputNames = settingDefinitions.Where(p => p.Scope == SettingScope.Application && p.ScriptAccess != SettingScriptAccess.None).Select(p => p.Name).ToArray();
        }

        protected abstract IReadOnlyDictionary<string, SettingDefinition> Init();

        protected abstract T SettingStoreRead<T>(string name, Guid tenantId, Guid userId, T defalutValue);

        protected abstract void SettingStoreWrite<T>(string name, T value, Guid tenantId, Guid userId);

        private T GetValueInternal<T>(string name, Guid? tenantId = null, Guid? userId = null)
        {
            T obj;

            var define = _definitions[name];

            //this implementation sucks, feel free to improve it.

            if (define.DefalutValue == null || false == define.DefalutValue.GetType().IsValueType)
            {
                if (typeof(T).IsValueType) throw new VApplicationFatalException("config value type mismatch");
            }
            else
            {
                if (false == typeof(T).IsValueType) throw new VApplicationFatalException("config value type mismatch");
            }

            switch (define.Scope)
            {
                default: throw new VApplicationFatalException("Setting scope not acceptable");
                // ReSharper disable PossibleInvalidOperationException 可空值报错要追究调用者
                case SettingScope.User: obj = SettingStoreRead(define.Name, tenantId ?? _session.TenantId.Value, userId ?? _session.UserId.Value, (T)define.DefalutValue); break;
                case SettingScope.Tenant: obj = SettingStoreRead(define.Name, tenantId ?? _session.TenantId.Value, Guid.Empty, (T)define.DefalutValue); break;
                // ReSharper restore PossibleInvalidOperationException
                case SettingScope.Application: obj = SettingStoreRead(define.Name, Guid.Empty, Guid.Empty, (T)define.DefalutValue); break;
            }

            return obj;
        }

        private void SetValueInternal(string name, object obj, SettingWriteLimit writer, Guid? tenantId = null, Guid? userId = null, SettingScope? scope = null)
        {
            var define = _definitions[name];

            var asWriter = writer;
            var writeToTenant = _session.TenantId;
            var writeToUser = _session.UserId;

            //系统拥有任意写入权限, 要求明确指定
            if (SettingWriteLimit.System == writer)
            {
                if (false == tenantId.HasValue) throw new ArgumentNullException(nameof(tenantId), "System writer should specific explicit target");
                if (false == userId.HasValue) throw new ArgumentNullException(nameof(userId), "System writer should specific explicit target");
                if (false == scope.HasValue) throw new ArgumentNullException(nameof(scope), "System writer should specific explicit target");

                asWriter = SettingWriteLimit.System;
                writeToTenant = tenantId.Value;
                writeToUser = userId.Value;
            }
            //写入当前租户的指定用户要求是 租户管理员
            else if (false == tenantId.HasValue && userId.HasValue && scope == SettingScope.User)
            {
                if (SettingWriteLimit.Admin != writer)
                    throw new VApplicationAuthException($"Write access denied, write to specific {scope} except {SettingWriteLimit.Admin} but got {writer}",AuthReason.AccessDenied);

                asWriter = SettingWriteLimit.User;
                writeToTenant = _session.GetTenantId();
                writeToUser = userId.Value;
            }
            //写入指定租户全局要求是 运营商
            else if (tenantId.HasValue && false == userId.HasValue && scope == SettingScope.Tenant)
            {
                if (SettingWriteLimit.Operator != writer)
                    throw new VApplicationAuthException($"Write access denied, write to specific {scope} except {SettingWriteLimit.Operator} but got {writer}", AuthReason.AccessDenied);

                asWriter = SettingWriteLimit.Admin;
                writeToTenant = tenantId.Value;
                writeToUser = Guid.Empty;
            }
            //写入指定租户的指定用户要求是 运营商
            else if (tenantId.HasValue && userId.HasValue && scope == SettingScope.User)
            {
                if (SettingWriteLimit.Operator != writer)
                    throw new VApplicationAuthException($"Write access denied, write to specific {scope} except {SettingWriteLimit.Operator} but got {writer}", AuthReason.AccessDenied);

                asWriter = SettingWriteLimit.Admin;
                writeToTenant = tenantId.Value;
                writeToUser = userId.Value;
            }

            if (asWriter != SettingWriteLimit.System && define.WriteLimit != asWriter) throw new VApplicationAuthException($"Write access denied, except {define.WriteLimit} or {SettingWriteLimit.System} but got {writer}", AuthReason.AccessDenied);

            //type check: this implementation sucks, feel free to improve it.

            if (define.DefalutValue == null || false == define.DefalutValue.GetType().IsValueType)
            {
                if (obj != null && obj.GetType().IsValueType) throw new VApplicationFatalException("config value type mismatch");
            }
            else if (define.DefalutValue.GetType().IsValueType)
            {
                if (obj == null || obj.GetType().IsValueType == false) throw new VApplicationFatalException("config value type mismatch");
            }

            //write to target
            switch (define.Scope)
            {
                default: throw new VApplicationFatalException("Setting define error: scope not acceptable");
                // ReSharper disable PossibleInvalidOperationException 可空值报错要追究调用者
                case SettingScope.User: SettingStoreWrite(define.Name, obj, writeToTenant.Value, writeToUser.Value); break;
                case SettingScope.Tenant: SettingStoreWrite(define.Name, obj, writeToTenant.Value, Guid.Empty); break;
                // ReSharper restore PossibleInvalidOperationException
                case SettingScope.Application: SettingStoreWrite(define.Name, obj, Guid.Empty, Guid.Empty); break;
            }
        }

        public void SetValueByUserScript(string name, object value)
        {
            if (false == _scriptWriteableNames.Contains(name)) throw new VApplicationFatalException("No script write access");
            SetValueByUser(name, value);
        }

        public void SetValueByUser(string name, object value)
        {
            SetValueInternal(name, value, SettingWriteLimit.User);
        }

        public void SetTenantValueByAdmin(string name, object value)
        {
            SetValueInternal(name, value, SettingWriteLimit.Admin);
        }

        public void SetTenantValueByOperator(string name, object value, Guid tenantId)
        {
            SetValueInternal(name, value, SettingWriteLimit.Operator, tenantId, scope: SettingScope.Tenant);
        }

        public void SetUserValueByAdmin(string name, object value, Guid userId)
        {
            SetValueInternal(name, value, SettingWriteLimit.Admin, null, userId, SettingScope.User);
        }

        public void SetUserValueByOperator(string name, object value, Guid tenantId, Guid userId)
        {
            SetValueInternal(name, value, SettingWriteLimit.Operator, tenantId, userId, SettingScope.User);
        }

        public void SetApplicationValueByOperator(string name, object value)
        {
            SetValueInternal(name, value, SettingWriteLimit.Operator);
        }

        public void SetAnyValueBySystem(string name, Guid tenantId, Guid userId, SettingScope scope, object value)
        {
            SetValueInternal(name, value, SettingWriteLimit.System, tenantId, userId, scope);
        }

        public T GetUserValue<T>(string name, T anon = default(T))
        {
            if (false == _userScopeNames.Contains(name)) throw new VApplicationFatalException("Missing setting define");
            return GetValueInternal<T>(name);
        }

        public T GetUserValueByAdmin<T>(string name, Guid userId, T anon = default(T))
        {
            if (false == _userScopeNames.Contains(name)) throw new VApplicationFatalException("Missing setting define");
            return GetValueInternal<T>(name, _session.GetTenantId());
        }

        public T GetUserValueByOperator<T>(string name, Guid tenantId, Guid userId, T anon = default(T))
        {
            if (false == _userScopeNames.Contains(name)) throw new VApplicationFatalException("Missing setting define");
            return GetValueInternal<T>(name, tenantId, userId);
        }

        public T GetTenantValue<T>(string name, T anon = default(T))
        {
            if (false == _tenantScopeNames.Contains(name)) throw new VApplicationFatalException("setting define missing");
            return GetValueInternal<T>(name);
        }

        public T GetTenantValueByOperator<T>(string name, Guid tenantId, T anon = default(T))
        {
            if (false == _tenantScopeNames.Contains(name)) throw new VApplicationFatalException("setting define missing");
            return GetValueInternal<T>(name, tenantId);
        }

        public T GetApplicationValue<T>(string name, T anon = default(T))
        {
            if (false == _applicationScopeNames.Contains(name)) throw new VApplicationFatalException("setting define missing");
            return GetValueInternal<T>(name);
        }

        public T GetAnyValueBySystem<T>(string name, Guid? tenantId, Guid? userId, T anon = default(T))
        {
            return GetValueInternal<T>(name, tenantId, userId);
        }

        public IDictionary<string, object> GetScriptOutput()
        {
            var names = _applicationScriptOutputNames;
            if (_session.UserId.HasValue) names = names.Union(_scriptOutputNames).ToArray();

            return names.ToDictionary(p => p, p => GetValueInternal<object>(p));
        }
    }
}
