using e_suite.API.Common;
using e_suite.API.Common.exceptions;
using e_suite.API.Common.models;
using e_suite.API.Common.models.@base;
using e_suite.API.Common.repository;
using e_suite.Database.Audit;
using e_suite.Database.Audit.Models;
using e_suite.Database.Core.Tables.CustomFields;
using e_suite.Utilities.Pagination;
using eSuite.Core.CustomFields;
using eSuite.Core.Miscellaneous;
using System.Linq.Expressions;
using System.Text.Json;

namespace e_suite.Modules.CustomFieldsManager;

public class CustomFieldManager : ICustomFieldManager
{
    private enum UpdateType
    {
        NoReferenceNeeded = 0,
        SameObjTypeReference = 1,
        DifferentObjectReference = 2
    }

    private readonly ICustomFieldRepository _customFieldRepository;
    private readonly ICustomFieldReferenceObjectRepository _customFieldReferenceObjectRepository;
    private readonly ICustomFieldHelper _customFieldHelper;
    private readonly IPatchFactory _patchFactory;

    public CustomFieldManager(ICustomFieldRepository customFieldRepository, ICustomFieldReferenceObjectRepository customFieldReferenceObjectRepository, ICustomFieldHelper customFieldHelper, IPatchFactory patchFactory)
    {
        _customFieldRepository = customFieldRepository;
        _customFieldReferenceObjectRepository = customFieldReferenceObjectRepository;
        _customFieldHelper = customFieldHelper;
        _patchFactory = patchFactory;
    }

    private async Task<ValueTuple<bool, long?>> ValidateFieldDefinition(FieldType fieldType, IGeneralIdRef? idRef, CancellationToken cancellationToken)
    {
        if (!ContainsReferenceType(fieldType))
            return (true, null);

        if (idRef == null)
            return (false, null);

        return await (fieldType switch
        {
            FieldType.Sequence => ValidateDelegate(() =>
                _customFieldReferenceObjectRepository.GetSequenceByGeneralRefIdAsync(idRef, cancellationToken)),
            FieldType.Glossary => ValidateDelegate(() =>
                _customFieldReferenceObjectRepository.GetGlossaryByGeneralRefIdAsync(idRef, cancellationToken)),
            _ => throw new ArgumentException("Add field type to the switch!"),
        });

    }

    private static async Task<(bool, long?)> ValidateDelegate<T>(Func<Task<T>> func) where T : IId?
    {
        return await func() is IId res
            ? (true, res!.Id)
            : (false, null);
    }

    private static bool ContainsReferenceType(FieldType value)
    => value is (FieldType.Sequence or FieldType.Glossary);

    private static void Assert(CustomField? customField)
    {
        AssertExists(customField);

        switch (customField!.FieldType)
        {
            case FieldType.Sequence:
                if (customField.MinEntries != 1)
#pragma warning disable CA2208 // Instantiate argument exceptions correctly
                    throw new ArgumentException($"Must be 1 for fields of type {customField.FieldType}", paramName: nameof(customField.MinEntries));

                if (customField.MaxEntries != 1)
                    throw new ArgumentException($"Must be 1 for fields of type {customField.FieldType}", paramName: nameof(customField.MaxEntries));
#pragma warning restore CA2208 // Instantiate argument exceptions correctly
                break;
        }
    }

    private static void AssertExists(CustomField? customField)
    {
        if (customField == null || customField.Deleted)
            throw new NotFoundException($"There is no CustomField with this Id");
    }

    private async Task<long?> AssertFieldReference(FieldType fieldType, GeneralIdRef? idRef, CancellationToken cancellationToken)
    {
        var res = await ValidateFieldDefinition(fieldType, idRef, cancellationToken);
        if (!res.Item1)
        {
            throw new InvalidReferenceObjectId($"There is no object of type {fieldType} with the given RefElementId ({idRef?.Id.ToString() ?? idRef?.Guid.ToString()})");
        }
        return res.Item2;
    }

    private async Task ValidateName(string customFieldName, GeneralIdRef idRef, CancellationToken cancellationToken)
    {
        var res = await _customFieldRepository.GetByNameAsync(customFieldName, cancellationToken);
        if (res != null)
        {
            if (res.Guid != idRef.Guid || res.Id != idRef.Id)
            {
                throw new ExistsException($"A Custom field with this name exists {customFieldName}");
            }
            return;
        }
        await ValidateName(customFieldName, cancellationToken);
    }
    private async Task ValidateName(string customFieldName, CancellationToken cancellationToken)
    {
        if (await _customFieldRepository.GetByNameAsync(customFieldName, cancellationToken) != null)
        {
            throw new ExistsException($"A Custom field with this name exists {customFieldName}");
        }
    }

    public async Task<IPaginatedData<CustomFieldDefinition>> GetFieldsAsync(Paging paging, CancellationToken cancellationToken)
    {
        var sequences = _customFieldRepository.GetCustomFieldList();

        var paginatedData = await PaginatedData.Paginate<CustomField, CustomFieldDefinition>(sequences, paging,
            KeySelector, cancellationToken);

        return paginatedData;
    }
    
    private Expression<Func<CustomField, object>> KeySelector(string? sortKey)
    {
        return sortKey?.ToLowerInvariant() switch
        {
            "id" => x => x.Id,
            "guid" => x => x.Guid,
            "fieldtype" => x => x.FieldType,
            "defaultvalue" => x => x.DefaultValue,
            _ => x => x.Name
        };
    }

    public async Task CreateFieldAsync(AuditUserDetails auditUserDetails, CreateCustomField customField, CancellationToken cancellationToken)
    {
        await _customFieldRepository.TransactionAsync(async () =>
        {
            await ValidateName(customField.Name, cancellationToken);

            if (customField.Guid != null && await _customFieldRepository.GetByIdAsync(new GeneralIdRef { Guid = customField.Guid }, cancellationToken) != null)
                throw new ExistsException("A Field with this id exists");

            var refId = await AssertFieldReference(customField.FieldType, customField.RefElementId!, cancellationToken);
            
            var customfFieldReal = new CustomField
            {
                DefaultValue = customField.DefaultValue,
                FieldType = customField.FieldType,
                Name = customField.Name,
                MaxEntries = customField.MaxEntries,
                MinEntries = customField.MinEntries,
                Guid = customField.Guid ?? Guid.NewGuid()
            };

            Assert(customfFieldReal);
            await _customFieldRepository.CreateAsync(auditUserDetails, customfFieldReal, cancellationToken);
            await DealWithParameters(auditUserDetails, customField, null, customfFieldReal, refId, cancellationToken);
        });
    }

    public async Task DeleteFieldAsync(AuditUserDetails auditUserDetails, IGeneralIdRef id, CancellationToken cancellationToken)
    {
        await _customFieldRepository.TransactionAsync(async () =>
        {
            var customField = await _customFieldRepository.GetByIdAsync(id, cancellationToken);
            AssertExists(customField);
            customField!.Deleted = true;
            await _customFieldRepository.EditAsync(auditUserDetails, customField!, cancellationToken);
        });
    }

    public async Task EditFieldAsync(
        AuditUserDetails auditUserDetails,
        EditCustomFields customField,
        CancellationToken cancellationToken
    )
    {
        await AlterField(auditUserDetails, customField.Id, async customFieldOriginal =>
        {
            var originalFieldType = customFieldOriginal!.FieldType;
            var refId = await AssertFieldReference(customField.FieldType, customField.RefElementId!, cancellationToken);

            customFieldOriginal.DefaultValue = customField.DefaultValue;
            customFieldOriginal.FieldType = customField.FieldType;

            if (customFieldOriginal.Name != customField.Name)
            {
                await ValidateName(customField.Name, customField.Id, cancellationToken);
                customFieldOriginal.Name = customField.Name;
            }

            customFieldOriginal.MaxEntries = customField.MaxEntries;
            customFieldOriginal.MinEntries = customField.MinEntries;

            await DealWithParameters(auditUserDetails, customField, originalFieldType, customFieldOriginal, refId, cancellationToken);
        }, cancellationToken);
    }

    private async Task AlterField(
        AuditUserDetails auditUserDetails,
        IGeneralIdRef Id,
        Func<CustomField, Task> applyChanges,
        CancellationToken cancellationToken
    )
    {
        await _customFieldRepository.TransactionAsync(async () =>
        {
            var customFieldOriginal = await _customFieldRepository.GetByIdAsync(Id, cancellationToken);
            Assert(customFieldOriginal);

            await applyChanges(customFieldOriginal);

            Assert(customFieldOriginal);
            await _customFieldRepository.EditAsync(auditUserDetails, customFieldOriginal, cancellationToken);
        });
    }

    public async Task PatchFieldAsync(
        AuditUserDetails auditUserDetails,
        IGeneralIdRef Id,
        PatchCustomFields customField,
        CancellationToken cancellationToken
    )
    {
        var patch = _patchFactory.Create(customField);

        await AlterField(auditUserDetails, Id, async editUser =>
        {
            await patch.ApplyTo(customField);
        }, cancellationToken);
    }

    private async Task DealWithParameters(
        AuditUserDetails auditUserDetails,
        CustomFieldBase customField,
        FieldType? originalFieldType,
        CustomField customFieldOriginal,
        long? refId,
        CancellationToken cancellationToken
    )
    {
        await FieldDeltaUpdate(FieldType.Glossary, customField, originalFieldType,
            async () => await _customFieldReferenceObjectRepository.CreateGlossaryReferenceAsync(auditUserDetails, customFieldOriginal!.Id, refId!.Value, cancellationToken),
            async () => {
                var customFieldGlossary = await _customFieldReferenceObjectRepository.GetGlossaryReferenceAsync(customFieldOriginal!.Id, cancellationToken);
                customFieldGlossary!.GlossaryId = refId!.Value;
                await _customFieldReferenceObjectRepository.EditGlossaryReferenceAsync(auditUserDetails, customFieldGlossary, cancellationToken);
            },
            async () => await _customFieldReferenceObjectRepository.DeleteGlossaryReferenceAsync(auditUserDetails, customFieldOriginal.Id, cancellationToken)
        );
        await FieldDeltaUpdate(FieldType.Sequence, customField, originalFieldType,
            async () => await _customFieldReferenceObjectRepository.CreateSequenceReferenceAsync(auditUserDetails, customFieldOriginal.Id, refId!.Value, cancellationToken),
            async () => {
                var customFieldsequence = await _customFieldReferenceObjectRepository.GetSequenceReferenceAsync(customFieldOriginal!.Id, cancellationToken);
                customFieldsequence!.SequenceId = refId!.Value;
                await _customFieldReferenceObjectRepository.EditSequenceReferenceAsync(auditUserDetails, customFieldsequence, cancellationToken);
            },
            async () => await _customFieldReferenceObjectRepository.DeleteSequenceReferenceAsync(auditUserDetails, customFieldOriginal.Id, cancellationToken)
        );
        await FieldDeltaUpdate(FieldType.Text, customField, originalFieldType,
            async () =>
            {
                var textFieldParameters = DeserializeParameters<TextFieldParameters>(customField);
                var customFieldText = new CustomFieldText
                {
                    CustomFieldId = customFieldOriginal.Id
                };
                SetCustomFieldTextParameters(textFieldParameters, customFieldText);

                await _customFieldReferenceObjectRepository.CreateCustomFieldTextsAsync(auditUserDetails, customFieldText, cancellationToken);
            },
            async () => {
                var textFieldParameters = DeserializeParameters<TextFieldParameters>(customField);
                var customFieldText = await GetCustomFieldText(customFieldOriginal, cancellationToken);
                SetCustomFieldTextParameters(textFieldParameters, customFieldText);

                await _customFieldReferenceObjectRepository.EditCustomFieldTextsAsync(auditUserDetails, customFieldText, textFieldParameters, cancellationToken);
            },
            async () =>
            {
                var customFieldText = await GetCustomFieldText(customFieldOriginal, cancellationToken);

                await _customFieldReferenceObjectRepository.DeleteCustomFieldTextsAsync(auditUserDetails, customFieldText, cancellationToken);
            }
        );
        await FieldDeltaUpdate(FieldType.Number, customField, originalFieldType,
            async () =>
            {
                var numberFieldParameters = DeserializeParameters<NumberFieldParameters>(customField);
                var customFieldNumber = new CustomFieldNumber
                {
                    CustomFieldId = customFieldOriginal.Id
                };
                SetCustomFieldNumberParameters(numberFieldParameters, customFieldNumber);

                await _customFieldReferenceObjectRepository.CreateCustomFieldNumbersAsync(auditUserDetails, customFieldNumber, cancellationToken);
            },
            async () => {
                var numberFieldParameters = DeserializeParameters<NumberFieldParameters>(customField);
                var customFieldNumber = await GetCustomFieldNumber(customFieldOriginal, cancellationToken);
                SetCustomFieldNumberParameters(numberFieldParameters, customFieldNumber);
                
                await _customFieldReferenceObjectRepository.EditCustomFieldNumbersAsync(auditUserDetails, customFieldNumber, numberFieldParameters, cancellationToken);
            },
            async () =>
            {
                var customFieldNumber = await GetCustomFieldNumber(customFieldOriginal, cancellationToken);

                await _customFieldReferenceObjectRepository.DeleteCustomFieldNumbersAsync(auditUserDetails, customFieldNumber, cancellationToken);
            });
    }

    private async Task<CustomFieldNumber> GetCustomFieldNumber(CustomField customFieldOriginal, CancellationToken cancellationToken)
    {
        var customFieldNumber =
            await _customFieldReferenceObjectRepository.GetNumbersParametersAsync(customFieldOriginal.Id,
                cancellationToken) ?? throw new NullReferenceException("unable to find existing custom field number");
        return customFieldNumber;
    }

    private async Task<CustomFieldText> GetCustomFieldText(CustomField customFieldOriginal, CancellationToken cancellationToken)
    {
        var customFieldText =
            await _customFieldReferenceObjectRepository.GetTextsParametersAsync(customFieldOriginal.Id,
                cancellationToken) ?? new CustomFieldText
            {
                CustomFieldId = customFieldOriginal.Id,
                MultiLine = false,
                CustomField = customFieldOriginal
            };
        return customFieldText;
    }

    private static void SetCustomFieldNumberParameters(NumberFieldParameters numberFieldParameters, CustomFieldNumber customFieldNumber)
    {
        customFieldNumber.MinimumValue = string.IsNullOrWhiteSpace(numberFieldParameters.MinimumValue) ? null : decimal.Parse(numberFieldParameters.MinimumValue);
        customFieldNumber.MaximumValue = string.IsNullOrWhiteSpace(numberFieldParameters.MaximumValue) ? null : decimal.Parse(numberFieldParameters.MaximumValue);
        customFieldNumber.Step = string.IsNullOrWhiteSpace(numberFieldParameters.Step) ? null : decimal.Parse(numberFieldParameters.Step);
    }

    private static void SetCustomFieldTextParameters(TextFieldParameters textFieldParameters, CustomFieldText customFieldText)
    {
        customFieldText.MultiLine = textFieldParameters.MultiLine;
    }

    private static T DeserializeParameters<T>(CustomFieldBase customField)
    {
        if (string.IsNullOrWhiteSpace(customField.Parameters))
            throw new NullReferenceException($"Parameters must be supplied for {customField.FieldType} fields");

        var numberFieldParameters = JsonSerializer.Deserialize<T>(customField.Parameters) ??
                                    throw new ArgumentException($"Parameters not correctly formatted for {customField.FieldType} field");
        return numberFieldParameters;
    }

    private static async Task FieldDeltaUpdate(FieldType fieldType, CustomFieldBase customField, FieldType? originalFieldType, Func<Task> add, Func<Task> edit, Func<Task> delete)
    {
        var originalIfFieldType = originalFieldType == fieldType;
        var newIfFieldType = customField.FieldType == fieldType;
        if (originalIfFieldType && newIfFieldType)
            await edit();
        else if (originalIfFieldType && !newIfFieldType)
            await delete();
        else if (!originalIfFieldType && newIfFieldType)
            await add();
    }


    public async Task<CustomFieldDefinition> GetFieldAsync(IGeneralIdRef id, CancellationToken cancellationToken)
    {
        var customField = await _customFieldRepository.GetByIdAsync(id, cancellationToken);
        Assert(customField);
        return await _customFieldHelper.TranslateToCustomFieldDefinitionAsync(customField!, cancellationToken);
    }

    public async Task<CustomFieldDefinition> GetFieldAsync(string name, CancellationToken cancellationToken)
    {
        var customField = await _customFieldRepository.GetByNameAsync(name, cancellationToken);
        if (customField == null || customField.Deleted)
        {
            throw new NotFoundException($"There is no CustomField with this name: {name}");
        }

        return await _customFieldHelper.TranslateToCustomFieldDefinitionAsync(customField, cancellationToken);
    }
}
