using System;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Dacryptero.Data.Schema;
using LiteDB;

namespace Dacryptero.Web.Events.Receive
{
    public class EditAttributeSend : ReceiveBase
    {
        public string Id { get; private set; } = "";

        public string Key { get; private set; } = "";

        public JsonElement Value { get; private set; }

        public override async Task Execute(ExecuteArgs args)
        {
#region VALIDATION PHASE
            // parse id
            var id = Id.ToObjectId();
            if (id is null)
            {
                await args.Connection.Send(new Send.EditDenied(Id, Key, Send.EditTarget.Attribute,
                    Send.EditDeniedReason.IdNotFound,
                    "invalid id"
                )).ConfigureAwait(false);
                return;
            }
            // get person
            var result = await args.Data.GetPersonAsync(id, CancellationToken.None)
                .ConfigureAwait(false);
            if (result is null)
            {
                await args.Connection.Send(new Send.EditDenied(Id, Key, Send.EditTarget.Attribute,
                    Send.EditDeniedReason.IdNotFound,
                    "id does not exists"
                )).ConfigureAwait(false);
                return;
            }
            if (result.Value.Value.Deleted)
            {
                await args.Connection.Send(new Send.EditDenied(Id, null,
                    Send.EditTarget.Attribute, Send.EditDeniedReason.Generic,
                    "person is deleted"
                )).ConfigureAwait(false);
                return;
            }
            // get schema
            if (!SchemaCollection.Schemas.TryGetValue(result.Value.Source.Config.RoleSchema, out SchemaFile? schema))
            {
                await args.Connection.Send(new Send.EditDenied(Id, Key,
                    Send.EditTarget.Attribute, Send.EditDeniedReason.SchemaNotFound,
                    "schema not found"
                )).ConfigureAwait(false);
                return;
            }
            if (!schema.CombinedRoles.TryGetValue(result.Value.Value.Role, out SchemaRole? role))
            {
                await args.Connection.Send(new Send.EditDenied(Id, Key,
                    Send.EditTarget.Interview, Send.EditDeniedReason.SchemaNotFound,
                    "schema not found"
                )).ConfigureAwait(false);
                return;
            }
            // verify if key exists
            if (!role.Attributes.TryGetValue(Key, out SchemaAttribute? attribute))
            {
                await args.Connection.Send(new Send.EditDenied(Id, Key,
                    Send.EditTarget.Attribute, Send.EditDeniedReason.AttributeNotAvailable,
                    $"attribute '{Key}' does not exists in schema"
                )).ConfigureAwait(false);
                return;
            }
            // verify if key is allowed
            if (attribute.Protected && result.Value.Value.ConfData is null)
            {
                await args.Connection.Send(new Send.EditDenied(Id, Key,
                    Send.EditTarget.Attribute, Send.EditDeniedReason.AttributeForbidden,
                    "confidential database is not attached - editing is not allowed"
                )).ConfigureAwait(false);
                return;
            }
            // verify the value type
            var value = ToBson(attribute.Type, Value);
            if (value is null)
            {
                await args.Connection.Send(new Send.EditDenied(Id, Key,
                    Send.EditTarget.Attribute, Send.EditDeniedReason.InvalidValueType,
                    $"the send json value ({Value.ValueKind}) cannot be transformed into a value for {attribute.Type}"
                )).ConfigureAwait(false);
                return;
            }
#endregion VALIDATION PHASE    
#region PERFORM CHANGE PHASE
            // create new entry
            var entry = new Data.AttributeEntry
            {
                Date = DateTime.UtcNow,
                Value = value,
            };
            // search for the attribute in the desired container
            var attributes = attribute.Protected 
                ? result.Value.Value.ConfData!.Attributes
                : result.Value.Value.Attributes;
            // get the attribute or create one
            bool @new;
            var attr = attributes.FirstOrDefault(x => x.Key == Key);
            Data.AttributeEntry? oldAttr = attr?.RecentEntry;
            if (attr is null)
            {
                attr = new Data.Attribute
                {
                    Created = entry.Date,
                    Key = Key,
                    PersonId = result.Value.Value.Id,
                };
                attributes.Add(attr);
                @new = true;
            }
            else @new = false;
            attr.Modified = entry.Date;
            attr.RecentEntry = entry;
            attr.Entries.Add(entry);
            result.Value.Value.Modified = entry.Date;
            await result.Value.Source.UpdateAttributeAsync(result.Value.Value, attr,
                attribute.Protected, CancellationToken.None
            );
            if (value.IsString)
            {
                if (@new || oldAttr is null || !oldAttr.Value.IsString)
                    result.Value.Source.SetIndex(Data.IndexSource.Persons, id, value.AsString,
                        attribute.Protected, false, args
                    );
                else result.Value.Source.ReplaceIndex(Data.IndexSource.Persons, id,
                    oldAttr.Value.AsString, value.AsString, attribute.Protected, args
                );
            }
            await args.Connection.Send(new Send.EditAccept(Id, Key, Send.EditTarget.Attribute))
                .ConfigureAwait(false);
#endregion PERFORM CHANGE PHASE
        }

        private BsonValue? ToBson(SchemaAttributeType type, JsonElement json)
        {
            string? stringValue;
            switch (type)
            {
                case SchemaAttributeType.DateTime:
                    if (json.TryGetDateTime(out DateTime dateTime))
                        return new BsonValue(dateTime);
                    else return null;
                case SchemaAttributeType.SingleLine:
                    if (json.ValueKind == JsonValueKind.String && (stringValue = json.GetString()) is not null)
                        return new BsonValue(stringValue);
                    else return null;
                case SchemaAttributeType.MultiLine:
                    if (json.ValueKind == JsonValueKind.String && (stringValue = json.GetString()) is not null)
                        return new BsonValue(stringValue);
                    else return null;
                case SchemaAttributeType.Number:
                    if (json.TryGetDouble(out double number))
                        return new BsonValue(number);
                    else return null;
                case SchemaAttributeType.Int:
                    if (json.TryGetInt64(out long longValue))
                        return new BsonValue(longValue);
                    else return null;
                case SchemaAttributeType.Bool:
                    if (json.ValueKind == JsonValueKind.True)
                        return new BsonValue(true);
                    else if (json.ValueKind == JsonValueKind.False)
                        return new BsonValue(false);
                    else return null;
                default: return null;
            }
        }

        public override void ReadJsonContent(JsonElement json)
        {
            Id = json.GetProperty("id").GetString() ?? "";
            Key = json.GetProperty("key").GetString() ?? "";
            Value = json.GetProperty("value");
        }
    }
}