namespace SumaTube.Crosscutting.Mappers
{
    using System;
    using System.Linq;
    using System.Reflection;

    public static class GenericMapper
    {
        public static TDestination MapTo<TSource, TDestination>(this TSource source)
            where TDestination : class
        {
            if (source == null)
                return default;

            var destType = typeof(TDestination);

            // Verifica se o tipo de destino tem um construtor público
            var constructors = destType.GetConstructors(BindingFlags.Public | BindingFlags.Instance)
                                      .OrderByDescending(c => c.GetParameters().Length)
                                      .ToList();

            // Se o tipo de destino tem um construtor com parâmetros, tenta mapear para ele
            if (constructors.Any() && constructors[0].GetParameters().Length > 0)
            {
                return MapToImmutableObject<TSource, TDestination>(source, constructors);
            }
            // Caso contrário, usa a abordagem de mapeamento de propriedades para objetos mutáveis
            else
            {
                return MapToMutableObject<TSource, TDestination>(source);
            }
        }

        private static TDestination MapToImmutableObject<TSource, TDestination>(TSource source, List<ConstructorInfo> constructors)
            where TDestination : class
        {
            var sourceType = typeof(TSource);
            var sourceProps = sourceType.GetProperties(BindingFlags.Public | BindingFlags.Instance);

            // Tenta cada construtor, começando pelo que tem mais parâmetros
            foreach (var constructor in constructors)
            {
                var parameters = constructor.GetParameters();
                if (parameters.Length == 0)
                {
                    // Construtor sem parâmetros, cria instância diretamente
                    var instance = constructor.Invoke(null);
                    return (TDestination)instance;
                }

                var parameterValues = new object[parameters.Length];
                bool canUseConstructor = true;

                // Tenta mapear os parâmetros do construtor
                for (int i = 0; i < parameters.Length; i++)
                {
                    var param = parameters[i];

                    // Procura propriedade com o mesmo nome (case insensitive)
                    var matchingProp = sourceProps.FirstOrDefault(p =>
                        string.Equals(p.Name, param.Name, StringComparison.OrdinalIgnoreCase));

                    if (matchingProp != null)
                    {
                        var value = matchingProp.GetValue(source);

                        // Se os tipos são compatíveis, usa o valor diretamente
                        if (param.ParameterType.IsAssignableFrom(matchingProp.PropertyType))
                        {
                            parameterValues[i] = value;
                        }
                        // Verifica se existe uma conversão implícita
                        else if (value != null && TryImplicitConversion(value, param.ParameterType, out var convertedValue))
                        {
                            parameterValues[i] = convertedValue;
                        }
                        // Se o valor é um tipo complexo, tenta mapear recursivamente
                        else if (value != null && !matchingProp.PropertyType.IsPrimitive &&
                                !matchingProp.PropertyType.Namespace.StartsWith("System"))
                        {
                            try
                            {
                                var method = typeof(GenericMapper).GetMethod(nameof(MapTo));
                                var genericMethod = method.MakeGenericMethod(matchingProp.PropertyType, param.ParameterType);
                                parameterValues[i] = genericMethod.Invoke(null, new[] { value });
                            }
                            catch
                            {
                                canUseConstructor = false;
                                break;
                            }
                        }
                        else
                        {
                            canUseConstructor = false;
                            break;
                        }
                    }
                    else
                    {
                        // Se não encontrou uma propriedade correspondente, verifica se o parâmetro é opcional
                        if (param.IsOptional)
                        {
                            parameterValues[i] = param.DefaultValue;
                        }
                        else
                        {
                            canUseConstructor = false;
                            break;
                        }
                    }
                }

                if (canUseConstructor)
                {
                    try
                    {
                        var instance = constructor.Invoke(parameterValues);
                        return (TDestination)instance;
                    }
                    catch
                    {
                        // Se falhou ao criar a instância, tenta o próximo construtor
                        continue;
                    }
                }
            }

            // Se não conseguiu usar nenhum construtor, lança exceção
            throw new InvalidOperationException(
                $"Não foi possível mapear {sourceType.Name} para {typeof(TDestination).Name} " +
                $"usando os construtores disponíveis. Verifique se os nomes das propriedades " +
                $"correspondem aos nomes dos parâmetros do construtor (case insensitive).");
        }

        private static TDestination MapToMutableObject<TSource, TDestination>(TSource source)
            where TDestination : class
        {
            var destType = typeof(TDestination);

            // Tenta criar uma instância usando o construtor sem parâmetros
            TDestination destination;
            try
            {
                destination = Activator.CreateInstance<TDestination>();
            }
            catch (Exception ex)
            {
                throw new InvalidOperationException(
                    $"Não foi possível criar uma instância de {destType.Name}. " +
                    $"Certifique-se de que a classe tenha um construtor sem parâmetros acessível.", ex);
            }

            var sourceType = typeof(TSource);
            var sourceProps = sourceType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
            var destProps = destType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty);

            foreach (var destProp in destProps)
            {
                if (!destProp.CanWrite)
                    continue;

                // Procura propriedade com o mesmo nome (case insensitive)
                var sourceProp = sourceProps.FirstOrDefault(p =>
                    string.Equals(p.Name, destProp.Name, StringComparison.OrdinalIgnoreCase));

                if (sourceProp != null)
                {
                    var value = sourceProp.GetValue(source);
                    if (value != null)
                    {
                        // Se os tipos são compatíveis, atribui diretamente
                        if (destProp.PropertyType.IsAssignableFrom(sourceProp.PropertyType))
                        {
                            destProp.SetValue(destination, value);
                        }
                        // Verifica se existe uma conversão implícita
                        else if (TryImplicitConversion(value, destProp.PropertyType, out var convertedValue))
                        {
                            destProp.SetValue(destination, convertedValue);
                        }
                        // Se o valor é um tipo complexo, tenta mapear recursivamente
                        else if (!sourceProp.PropertyType.IsPrimitive &&
                                !sourceProp.PropertyType.Namespace.StartsWith("System"))
                        {
                            try
                            {
                                var method = typeof(GenericMapper).GetMethod(nameof(MapTo));
                                var genericMethod = method.MakeGenericMethod(sourceProp.PropertyType, destProp.PropertyType);
                                var mappedValue = genericMethod.Invoke(null, new[] { value });
                                destProp.SetValue(destination, mappedValue);
                            }
                            catch
                            {
                                // Ignora se não conseguir mapear
                            }
                        }
                    }
                }
            }

            return destination;
        }

        private static bool TryImplicitConversion(object source, Type destinationType, out object result)
        {
            result = null;
            if (source == null) return false;

            var sourceType = source.GetType();

            // Verifica operador de conversão implícita no tipo de origem
            var methodSource = sourceType.GetMethods(BindingFlags.Public | BindingFlags.Static)
                .FirstOrDefault(m =>
                    m.Name == "op_Implicit" &&
                    m.ReturnType == destinationType &&
                    m.GetParameters().Length == 1 &&
                    m.GetParameters()[0].ParameterType == sourceType);

            if (methodSource != null)
            {
                result = methodSource.Invoke(null, new[] { source });
                return true;
            }

            // Verifica operador de conversão implícita no tipo de destino
            var methodDest = destinationType.GetMethods(BindingFlags.Public | BindingFlags.Static)
                .FirstOrDefault(m =>
                    m.Name == "op_Implicit" &&
                    m.ReturnType == destinationType &&
                    m.GetParameters().Length == 1 &&
                    m.GetParameters()[0].ParameterType == sourceType);

            if (methodDest != null)
            {
                result = methodDest.Invoke(null, new[] { source });
                return true;
            }

            // Tenta converter usando Convert.ChangeType
            try
            {
                if (destinationType.IsValueType || destinationType == typeof(string))
                {
                    result = Convert.ChangeType(source, destinationType);
                    return true;
                }
            }
            catch { }

            return false;
        }
    }
}
