package com.imprimelibros.erp.shared.validation;

import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;

import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.FlushModeType;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.PersistenceUnit;
import jakarta.persistence.criteria.*;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

import org.springframework.stereotype.Component;

@Component
public class NoRangeOverlapValidator implements ConstraintValidator<NoRangeOverlap, Object> {

    @PersistenceUnit
    private EntityManagerFactory emf;

    private String minField;
    private String maxField;
    private String idField;
    private String[] partitionFields;
    private String deletedFlag;
    private boolean deletedActiveValue;
    private String message;
    private String invalidRangeMessage;

    @Override
    public void initialize(NoRangeOverlap ann) {
        this.minField = ann.min();
        this.maxField = ann.max();
        this.idField = ann.id();
        this.partitionFields = ann.partitionBy();
        this.deletedFlag = ann.deletedFlag();
        this.deletedActiveValue = ann.deletedActiveValue();
        this.message = ann.message();
        this.invalidRangeMessage = ann.invalidRangeMessage();
    }

    @Override
    public boolean isValid(Object bean, ConstraintValidatorContext ctx) {
        if (bean == null)
            return true;
        EntityManager em = null;
        try {
            // EM aislado para evitar auto-flush durante la validación
            em = emf.createEntityManager();
            em.setFlushMode(FlushModeType.COMMIT);

            Class<?> entityClass = bean.getClass();

            Number min = (Number) read(bean, minField);
            Number max = (Number) read(bean, maxField);
            Object id = safeRead(bean, idField); // puede ser null en INSERT

            if (min == null || max == null)
                return true;

            if (min.longValue() > max.longValue()) {
                ctx.disableDefaultConstraintViolation();
                ctx.buildConstraintViolationWithTemplate(invalidRangeMessage)
                        .addPropertyNode(maxField)
                        .addConstraintViolation();
                return false;
            }

            CriteriaBuilder cb = em.getCriteriaBuilder();
            CriteriaQuery<Long> cq = cb.createQuery(Long.class);
            Root<?> root = cq.from(entityClass);

            cq.select(cb.count(root));

            Predicate pred = cb.conjunction();

            if (id != null) {
                pred = cb.and(pred, cb.notEqual(root.get(idField), id));
            }

            for (String pf : partitionFields) {
                Object val = read(bean, pf);
                pred = cb.and(pred, cb.equal(root.get(pf), val));
            }

            if (!deletedFlag.isEmpty()) {
                pred = cb.and(pred, cb.equal(root.get(deletedFlag), deletedActiveValue));
            }

            Expression<Number> eMin = root.get(minField);
            Expression<Number> eMax = root.get(maxField);

            Predicate noOverlap = cb.or(
                    cb.lt(eMax.as(Long.class), min.longValue()),
                    cb.gt(eMin.as(Long.class), max.longValue()));
            Predicate overlap = cb.not(noOverlap);

            cq.where(cb.and(pred, overlap));

            Long count = em.createQuery(cq).getSingleResult();
            if (count != null && count > 0) {
                ctx.disableDefaultConstraintViolation();
                ctx.buildConstraintViolationWithTemplate(message)
                        .addPropertyNode(minField).addConstraintViolation();
                ctx.buildConstraintViolationWithTemplate(message)
                        .addPropertyNode(maxField).addConstraintViolation();
                return false;
            }

            return true;

        } catch (Exception ex) {
            // En caso de error inesperado, puedes loguear aquí
            return true;
        }
    }

    private Object read(Object bean, String name) throws Exception {
        PropertyDescriptor pd = getPropertyDescriptor(bean.getClass(), name);
        Method getter = pd.getReadMethod();
        return getter.invoke(bean);
    }

    private Object safeRead(Object bean, String name) {
        try {
            return read(bean, name);
        } catch (Exception ignore) {
            return null;
        }
    }

    private PropertyDescriptor getPropertyDescriptor(Class<?> clazz, String name) throws IntrospectionException {
        return new PropertyDescriptor(name, clazz);
    }
}
