using InABox.Clients;
using InABox.Configuration;
using InABox.Core.Postable;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;

namespace InABox.Core
{
    public interface IGlobalSettingsPosterEngine
    {
        IGlobalPosterSettings GetGlobalSettings();
        void SaveGlobalSettings(IGlobalPosterSettings settings);
    }

    /// <summary>
    /// Marks an <see cref="IPosterEngine{TPostable}"/> as having global settings, not specific to certain entities.
    /// </summary>
    /// <typeparam name="TGlobalSettings"></typeparam>
    public interface IGlobalSettingsPosterEngine<TPoster, TGlobalSettings> : IGlobalSettingsPosterEngine
        where TPoster : IGlobalSettingsPoster<TGlobalSettings>
        where TGlobalSettings : BaseObject, IGlobalPosterSettings, new()
    {
        new TGlobalSettings GetGlobalSettings() => PosterUtils.LoadGlobalPosterSettings<TGlobalSettings>();
        void SaveGlobalSettings(TGlobalSettings settings) => PosterUtils.SaveGlobalPosterSettings(settings);

        IGlobalPosterSettings IGlobalSettingsPosterEngine.GetGlobalSettings() => GetGlobalSettings();
        void IGlobalSettingsPosterEngine.SaveGlobalSettings(IGlobalPosterSettings settings)
        {
            if(settings is TGlobalSettings globalSettings)
            {
                SaveGlobalSettings(globalSettings);
            }
        }
    }

    public interface IPosterEngine<TPostable>
        where TPostable : Entity, IPostable, IRemotable, IPersistent, new()
    {
        IPostResult<TPostable>? Process(IDataModel<TPostable> model);
    }

    public interface IPosterEngine<TPostable, TPoster, TSettings> : IPosterEngine<TPostable>
        where TPostable : Entity, IPostable, IRemotable, IPersistent, new()
        where TPoster : IPoster<TPostable, TSettings>
        where TSettings : PosterSettings, new()
    {
    }

    /// <summary>
    /// A base class for all <see cref="IPosterEngine{TPostable}"/>. A concrete instance of this will be loaded by
    /// <see cref="PosterUtils.Process{T}(IDataModel{T})"/>; a new instance is guaranteed to be created each time that method is called.
    /// </summary>
    /// <typeparam name="TPostable"></typeparam>
    /// <typeparam name="TPoster"></typeparam>
    /// <typeparam name="TSettings"></typeparam>
    public abstract class PosterEngine<TPostable, TPoster, TSettings> : IPosterEngine<TPostable, TPoster, TSettings>
        where TPostable : Entity, IPostable, IRemotable, IPersistent, new()
        where TPoster : class, IPoster<TPostable, TSettings>
        where TSettings : PosterSettings, new()
    {
        protected TPoster Poster;

        public PosterEngine()
        {
            Poster = CreatePoster();
        }

        private static readonly Type? PosterType = PosterUtils.GetPoster(typeof(TPoster));

        protected virtual TPoster CreatePoster()
        {
            var poster = (Activator.CreateInstance(PosterType!) as TPoster)!;
            poster.Settings = GetSettings();

            if(this is IGlobalSettingsPosterEngine globalSettingsEngine && poster is IGlobalSettingsPoster globalSettingsPoster)
            {
                globalSettingsPoster.GlobalSettings = globalSettingsEngine.GetGlobalSettings();
            }

            return poster;
        }

        private TSettings? _settings;
        protected TSettings GetSettings()
        {
            _settings ??= PosterUtils.LoadPosterSettings<TPostable, TSettings>();
            return _settings;
        }

        protected void SaveSettings(TSettings settings)
        {
            _settings = settings;
            PosterUtils.SavePosterSettings<TPostable, TSettings>(_settings);
        }

        /// <summary>
        /// Returns the <see cref="TSettings.Script"/>, if <see cref="TSettings.ScriptEnabled"/> is <see langword="true"/>;
        /// otherwise, returns <see langword="null"/>.
        /// </summary>
        protected string? GetScript()
        {
            var settings = GetSettings();
            return settings.ScriptEnabled ? settings.Script : null;
        }

        protected abstract IPostResult<TPostable> DoProcess(IDataModel<TPostable> model);

        /// <summary>
        /// Process the <paramref name="model"/> before loading;
        /// </summary>
        /// <param name="model"></param>
        /// <returns><see langword="false"/> if the processing must be cancelled.</returns>
        public abstract bool BeforePost(IDataModel<TPostable> model);

        /// <summary>
        /// Prior to saving the <typeparamref name="TPostable"/> entities, make any necessary changes to those entities.
        /// This is only called if <see cref="Process(IDataModel{TPostable})"/> returned <see langword="true"/>.
        /// </summary>
        /// <param name="model"></param>
        /// <param name="result">The result object returned by <see cref="DoProcess(IDataModel{TPostable})"/></param>
        public abstract void AfterPost(IDataModel<TPostable> model, IPostResult<TPostable> result);

        private static void SetFailed(IList<TPostable> entities)
        {
            foreach (var post in entities)
            {
                post.PostedStatus = PostedStatus.PostFailed;
                post.PostedNote = "Post failed.";
            }
            new Client<TPostable>().Save(entities, "Post failed by user.");
        }

        public IPostResult<TPostable>? Process(IDataModel<TPostable> model)
        {
            if (!BeforePost(model))
            {
                return null;
            }

            // Add posted flags; if the columns is null, then we don't need to worry, because it will be loaded.
            model.GetColumns<TPostable>()?.Add(x => x.PostedStatus)
                .Add(x => x.Posted)
                .Add(x => x.PostedNote);

            model.LoadModel();

            var data = model.GetTable<TPostable>();

            if (!data.Rows.Any())
            {
                throw new EmptyPostException();
            }
            if(data.Rows.Any(x => x.Get<TPostable, PostedStatus>(x => x.PostedStatus) == PostedStatus.Posted))
            {
                throw new RepostedException();
            }

            try
            {
                var result = DoProcess(model);
                AfterPost(model, result);

                Client.Save(result.PostedEntities, "Posted by user.");

                foreach (var (type, fragments) in result.Fragments)
                {
                    Client.Create(type).Save(fragments.Cast<Entity>(), "");
                }
                return result;
            }
            catch (PostCancelledException)
            {
                var entities = data.ToObjects<TPostable>().ToList();
                SetFailed(entities);
                throw;
            }
            catch (MissingSettingException)
            {
                throw;
            }
            catch (PostFailedMessageException)
            {
                throw;
            }
            catch(Exception e)
            {
                Logger.Send(LogType.Error, "", $"Post Failed: {CoreUtils.FormatException(e)}");

                var entities = data.ToObjects<TPostable>().ToList();
                SetFailed(entities);
                throw;
            }
        }
    }

    public interface IPullerEngine<TPostable>
        where TPostable : Entity, IPostable, IRemotable, IPersistent, new()
    {
        IPullResult<TPostable>? Pull();
    }

    public interface IPullerEngine<TPostable, TPoster> : IPullerEngine<TPostable>
        where TPostable : Entity, IPostable, IRemotable, IPersistent, new()
    {
        IPullResult<TPostable> DoPull();

        IPullResult<TPostable>? IPullerEngine<TPostable>.Pull()
        {
            try
            {
                var result = DoPull();

                return result;
            }
            catch (PullCancelledException)
            {
                throw;
            }
            catch (MissingSettingException)
            {
                throw;
            }
            catch (PullFailedMessageException)
            {
                throw;
            }
            catch(Exception e)
            {
                Logger.Send(LogType.Error, "", $"Post Failed: {CoreUtils.FormatException(e)}");
                throw;
            }
        }
    }
}
