using e_suite.API.Common.exceptions;
using e_suite.Database.Core;
using e_suite.Database.Core.Extensions;
using e_suite.Database.Core.Models;
using eSuite.Core.Miscellaneous;
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations.Schema;
using System.Reflection;

namespace e_suite.API.Common;

public class Patch<T> : IPatch<T>
{
    private readonly IEsuiteDatabaseDbContext _dbContext;
    private readonly Type _targetType;
    public T Value { get; }

    public Patch(T value, IEsuiteDatabaseDbContext dbContext)
    {
        Value = value;
        _dbContext = dbContext;

        var patchesAttr = typeof(T).GetCustomAttribute<PatchesAttribute>();
        if (patchesAttr == null)
            throw new InvalidDataException(
                $"Patch DTO '{typeof(T).Name}' is missing required [Patches] attribute.");

        _targetType = patchesAttr.TargetType;
    }

    public async Task ApplyTo(object target)
    {
        if (target == null)
            throw new ArgumentNullException(nameof(target));

        if (!_targetType.IsAssignableFrom(target.GetType()))
        {
            throw new InvalidDataException(
                $"Patch DTO '{typeof(T).Name}' is defined to patch '{_targetType.Name}', " +
                $"but was applied to instance of '{target.GetType().Name}'.");
        }


        var targetType = target.GetType();
        var targetProps = targetType
            .GetProperties(BindingFlags.Public | BindingFlags.Instance)
            .ToDictionary(p => p.Name);

        var patchProps = typeof(T)
            .GetProperties(BindingFlags.Public | BindingFlags.Instance);

        var mappedProps = patchProps
            .Where(p => p.GetCustomAttribute<PatchMapAttribute>() != null)
            .ToList();

        if (mappedProps.Count == 0)
        {
            throw new InvalidDataException(
                $"Patch DTO '{typeof(T).Name}' does not define any properties with PatchMapAttribute.");
        }

        foreach (var patchProp in patchProps)
        {
            // Only process properties with PatchMap
            var mapAttr = patchProp.GetCustomAttribute<PatchMapAttribute>();
            if (mapAttr == null)
                continue;

            var targetPropName = mapAttr.TargetPropertyName;

            if (!targetProps.TryGetValue(targetPropName, out var targetProp))
                throw new InvalidDataException(
                    $"PatchMap refers to unknown property '{targetPropName}' on '{targetType.Name}'");

            if (!targetProp.CanWrite)
                throw new InvalidDataException(
                    $"Target property '{targetPropName}' is not writable");

            var patchValue = patchProp.GetValue(Value);

            // Null means "not included in the patch"
            if (patchValue == null)
                continue;

            bool isGeneralIdRef = typeof(IGeneralIdRef).IsAssignableFrom(patchProp.PropertyType);

            // Disallow nested objects (except string and IGeneralIdRef)
            if (patchProp.PropertyType.IsClass &&
                patchProp.PropertyType != typeof(string) &&
                !isGeneralIdRef)
            {
                throw new InvalidDataException(
                    $"Nested objects are not allowed in PATCH DTOs. Property: {patchProp.Name}");
            }

            // Handle navigation properties via IGeneralIdRef
            if (isGeneralIdRef)
            {
                // Ensure the target navigation type implements IGeneralId
                if (!typeof(IGeneralId).IsAssignableFrom(targetProp.PropertyType))
                {
                    throw new InvalidDataException(
                        $"PatchMap for '{patchProp.Name}' refers to navigation property '{targetPropName}', " +
                        $"but '{targetProp.PropertyType.Name}' does not implement IGeneralId. " +
                        $"Navigation properties patched via IGeneralIdRef must implement IGeneralId.");
                }

                var refValue = (IGeneralIdRef)patchValue;

                var targetEntityType = targetProp.PropertyType;

                // Get DbSet<TEntity>
                var setMethod = typeof(DbContext)
                    .GetMethod(nameof(DbContext.Set), Type.EmptyTypes)!
                    .MakeGenericMethod(targetEntityType);

                var dbSet = setMethod.Invoke(_dbContext, null);
                
                // Call FindByGeneralIdRefAsync<TEntity>
                var findMethod = typeof(QueryableExtensions)
                    .GetMethod(nameof(QueryableExtensions.FindByGeneralIdRefAsync))!
                    .MakeGenericMethod(targetEntityType);

                var task = (Task)findMethod.Invoke(
                    null,
                    [dbSet, refValue, CancellationToken.None]
                )!;

                await task.ConfigureAwait(false);

                var resultProperty = task.GetType().GetProperty("Result");
                var resolvedEntity = resultProperty?.GetValue(task);

                if (resolvedEntity == null)
                    throw new NotFoundException(
                        $"Unable to resolve reference for '{patchProp.Name}'");

                // Assign navigation property
                targetProp.SetValue(target, resolvedEntity);

                // Look for ForeignKeyAttribute on the navigation property
                var fkAttr = targetProp.GetCustomAttribute<ForeignKeyAttribute>();
                if (fkAttr != null)
                {
                    var fkName = fkAttr.Name;

                    if (targetProps.TryGetValue(fkName, out var fkProp))
                    {
                        var idValue = resolvedEntity
                            .GetType()
                            .GetProperty("Id")
                            ?.GetValue(resolvedEntity);

                        fkProp.SetValue(target, idValue);
                    }
                }

                continue;
            }

            // Default scalar assignment
            targetProp.SetValue(target, patchValue);
        }
    }
}