package com.caliverse.admin.history;

import com.caliverse.admin.domain.adminlog.FieldChange;
import com.caliverse.admin.dynamodb.domain.DocAttributeHandler;
import com.caliverse.admin.dynamodb.domain.doc.DynamoDBDocBase;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;

import java.text.SimpleDateFormat;
import java.util.*;

public class ChangeDetector {
    private static final ObjectMapper objectMapper = new ObjectMapper()
            .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
            .registerModule(new JavaTimeModule())
            .setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

    // 제외할 필드 목록
    private static final Set<String> EXCLUDED_FIELDS = new HashSet<>(Arrays.asList(
            "update_dt",
            "update_by",
            "created_dt",
            "created_by"
    ));

    @SuppressWarnings("unchecked")
    public static List<FieldChange> detectInsertChanges(DynamoDBDocBase newDoc) {
        Object attrib = DocAttributeHandler.getAttribValue(newDoc);
        if (attrib == null) {
            return new ArrayList<>();
        }

        return (attrib instanceof String)
                ? detectChangesFromJsonString((String) attrib)
                : detectChangesFromObject(attrib);
    }

    private static List<FieldChange> detectChangesFromJsonString(String jsonString) {
        try {
            Map<String, Object> attribMap = objectMapper.readValue(jsonString,
                    new TypeReference<LinkedHashMap<String, Object>>() {});
            return createFieldChanges(attribMap);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("Error processing JSON string changes", e);
        }
    }

    private static List<FieldChange> detectChangesFromObject(Object attrib) {
        Map<String, Object> attribMap = objectMapper.convertValue(attrib,
                new TypeReference<LinkedHashMap<String, Object>>() {});
        return createFieldChanges(attribMap);
    }

//    @SuppressWarnings("unchecked")
//    public static List<FieldChange> detectInsertChanges(DynamoDBDocBase newDoc) {
//        List<FieldChange> changes = new ArrayList<>();
//        Object attrib = DocAttributeHandler.getAttribValue(newDoc);
//
//        if (attrib == null) {
//            return changes;
//        }
//
//        Map<String, Object> attribMap = objectMapper.convertValue(attrib, Map.class);
//
//        for (Map.Entry<String, Object> entry : attribMap.entrySet()) {
//            changes.add(new FieldChange(
//                    entry.getKey(),
//                    null,
//                    entry.getValue()
//            ));
//        }
//
//        return changes;
//    }

    public static <T> List<FieldChange> detectInsertChanges(T newObj) {
        List<FieldChange> changes = new ArrayList<>();

        if (newObj == null) {
            return changes;
        }

        Map<String, Object> newObjMap = objectMapper.convertValue(newObj, Map.class);

        for (Map.Entry<String, Object> entry : newObjMap.entrySet()) {
            String fieldName = entry.getKey();
            Object newValue = entry.getValue();
            if (isExcludedField(fieldName)) {
                continue;
            }
            if (newValue instanceof String && isDateTimeString((String) newValue)) {
                newValue = ((String) newValue).replace('T', ' ');
            }
            changes.add(new FieldChange(
                    fieldName,
                    null,
                    newValue
            ));
        }

        return changes;
    }

    @SuppressWarnings("unchecked")
    public static List<FieldChange> detectChanges(DynamoDBDocBase beforeDoc, DynamoDBDocBase afterDoc) {
        List<FieldChange> changes = new ArrayList<>();

        if (beforeDoc == null || afterDoc == null) {
            return changes;
        }

        Object beforeAttrib = DocAttributeHandler.getAttribValue(beforeDoc);
        Object afterAttrib = DocAttributeHandler.getAttribValue(afterDoc);

        if (beforeAttrib == null || afterAttrib == null) {
            return changes;
        }

        try {
            Map<String, Object> oldAttribMap = convertToMap(beforeAttrib);
            Map<String, Object> newAttribMap = convertToMap(afterAttrib);

            for (Map.Entry<String, Object> entry : newAttribMap.entrySet()) {
                String fieldName = entry.getKey();
                Object newValue = entry.getValue();
                Object oldValue = oldAttribMap.get(fieldName);

                if (!isEqual(oldValue, newValue)) {
                    changes.add(new FieldChange(fieldName, oldValue, newValue));
                }
            }

            for (String fieldName : oldAttribMap.keySet()) {
                if (!newAttribMap.containsKey(fieldName)) {
                    changes.add(new FieldChange(fieldName, oldAttribMap.get(fieldName), null));
                }
            }

            return changes;
        } catch (JsonProcessingException e) {
            throw new RuntimeException("Error processing document changes", e);
        }
    }

//    @SuppressWarnings("unchecked")
//    public static List<FieldChange> detectChanges(DynamoDBDocBase beforeDoc, DynamoDBDocBase afterDoc) {
//        List<FieldChange> changes = new ArrayList<>();
//
//        if (beforeDoc == null || afterDoc == null) {
//            return changes;
//        }
//
//        Object beforeAttrib = DocAttributeHandler.getAttribValue(beforeDoc);
//        Object afterAttrib = DocAttributeHandler.getAttribValue(afterDoc);
//
//        if (beforeAttrib == null || afterAttrib == null) {
//            return changes;
//        }
//
//        Map<String, Object> oldAttribMap = objectMapper.convertValue(beforeAttrib, Map.class);
//        Map<String, Object> newAttribMap = objectMapper.convertValue(afterAttrib, Map.class);
//
//        for (Map.Entry<String, Object> entry : newAttribMap.entrySet()) {
//            String fieldName = entry.getKey();
//            Object newValue = entry.getValue();
//            Object oldValue = oldAttribMap.get(fieldName);
//
//            if (!isEqual(oldValue, newValue)) {
//                changes.add(new FieldChange(fieldName, oldValue, newValue));
//            }
//        }
//
//        for (String fieldName : oldAttribMap.keySet()) {
//            if (!newAttribMap.containsKey(fieldName)) {
//                changes.add(new FieldChange(fieldName, oldAttribMap.get(fieldName), null));
//            }
//        }
//
//        return changes;
//    }

    public static <T> List<FieldChange> detectChanges(T beforeObj, T afterObj) {
        List<FieldChange> changes = new ArrayList<>();

        if (beforeObj == null || afterObj == null) {
            return changes;
        }

        Map<String, Object> oldObjMap = objectMapper.convertValue(beforeObj, Map.class);
        Map<String, Object> newObjMap = objectMapper.convertValue(afterObj, Map.class);

        // 새로운 값의 모든 필드 확인
        for (Map.Entry<String, Object> entry : newObjMap.entrySet()) {
            String fieldName = entry.getKey();
            if (isExcludedField(fieldName)) {
                continue;
            }
            Object newValue = entry.getValue();
            Object oldValue = oldObjMap.get(fieldName);

            if (newValue instanceof String && isDateTimeString((String) newValue)) {
                newValue = ((String) newValue).replace('T', ' ');
            }
            if (oldValue instanceof String && isDateTimeString((String) oldValue)) {
                oldValue = ((String) oldValue).replace('T', ' ');
            }

            if (!isEqual(oldValue, newValue)) {
                changes.add(new FieldChange(fieldName, oldValue, newValue));
            }
        }

        // 삭제된 필드 확인
        for (String fieldName : oldObjMap.keySet()) {
            if (!newObjMap.containsKey(fieldName)) {
                changes.add(new FieldChange(fieldName, oldObjMap.get(fieldName), null));
            }
        }

        return changes;
    }

    @SuppressWarnings("unchecked")
    public static List<FieldChange> detectDeleteChanges(DynamoDBDocBase deletedDoc) {
        Object attrib = DocAttributeHandler.getAttribValue(deletedDoc);
        if (attrib == null) {
            return new ArrayList<>();
        }

        return (attrib instanceof String)
                ? detectDeleteChangesFromJsonString((String) attrib)
                : detectDeleteChangesFromObject(attrib);
    }

    private static List<FieldChange> detectDeleteChangesFromJsonString(String jsonString) {
        try {
            Map<String, Object> attribMap = objectMapper.readValue(jsonString,
                    new TypeReference<LinkedHashMap<String, Object>>() {});
            return createFieldChanges(attribMap);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("Error processing JSON string changes for delete", e);
        }
    }

    private static List<FieldChange> detectDeleteChangesFromObject(Object attrib) {
        Map<String, Object> attribMap = objectMapper.convertValue(attrib,
                new TypeReference<LinkedHashMap<String, Object>>() {});
        return createFieldChanges(attribMap);
    }

//    @SuppressWarnings("unchecked")
//    public static List<FieldChange> detectDeleteChanges(DynamoDBDocBase deletedDoc) {
//        List<FieldChange> changes = new ArrayList<>();
//        Object attrib = DocAttributeHandler.getAttribValue(deletedDoc);
//
//        if (attrib == null) {
//            return changes;
//        }
//
//        Map<String, Object> attribMap = objectMapper.convertValue(attrib, Map.class);
//
//        for (Map.Entry<String, Object> entry : attribMap.entrySet()) {
//            changes.add(new FieldChange(
//                    entry.getKey(),
//                    entry.getValue(),
//                    null
//            ));
//        }
//
//        return changes;
//    }

    public static <T> List<FieldChange> detectDeleteChanges(T deletedObj) {
        List<FieldChange> changes = new ArrayList<>();

        if (deletedObj == null) {
            return changes;
        }

        Map<String, Object> objMap = objectMapper.convertValue(deletedObj, Map.class);

        for (Map.Entry<String, Object> entry : objMap.entrySet()) {
            String fieldName = entry.getKey();
            if (isExcludedField(fieldName)) {
                continue;
            }
            Object oldValue = entry.getValue();
            if (oldValue instanceof String && isDateTimeString((String) oldValue)) {
                oldValue = ((String) oldValue).replace('T', ' ');
            }
            changes.add(new FieldChange(
                    fieldName,
                    oldValue,
                    null
            ));
        }

        return changes;
    }

    private static boolean isExcludedField(String fieldName) {
        return EXCLUDED_FIELDS.contains(fieldName) ||
                EXCLUDED_FIELDS.contains(toSnakeCase(fieldName)) ||
                EXCLUDED_FIELDS.contains(toCamelCase(fieldName));
    }

    private static String toSnakeCase(String str) {
        return str.replaceAll("([a-z])([A-Z])", "$1_$2").toLowerCase();
    }

    private static String toCamelCase(String str) {
        String[] words = str.split("[\\W_]+");
        StringBuilder builder = new StringBuilder();
        builder.append(words[0].toLowerCase());
        for (int i = 1; i < words.length; i++) {
            builder.append(words[i].substring(0, 1).toUpperCase());
            builder.append(words[i].substring(1).toLowerCase());
        }
        return builder.toString();
    }

    private static boolean isDateTimeString(String value) {
        return value != null && value.matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.*");
    }


    private static boolean isEqual(Object obj1, Object obj2) {
        if (obj1 == null && obj2 == null) return true;
        if (obj1 == null || obj2 == null) return false;
        return obj1.equals(obj2);
    }

    private static List<FieldChange> createFieldChanges(Map<String, Object> attribMap) {
        List<FieldChange> changes = new ArrayList<>();
        for (Map.Entry<String, Object> entry : attribMap.entrySet()) {
            changes.add(new FieldChange(
                    entry.getKey(),
                    null,
                    entry.getValue()
            ));
        }
        return changes;
    }

    private static Map<String, Object> convertToMap(Object attrib) throws JsonProcessingException {
        if (attrib instanceof String) {
            return objectMapper.readValue((String) attrib,
                    new TypeReference<LinkedHashMap<String, Object>>() {});
        }
        return objectMapper.convertValue(attrib,
                new TypeReference<LinkedHashMap<String, Object>>() {});
    }
}
