package at.procon.dip.domain.document.service;

import at.procon.dip.domain.document.DocumentAttributeValueType;
import at.procon.dip.domain.document.entity.Document;
import at.procon.dip.domain.document.entity.DocumentAttribute;
import at.procon.dip.domain.document.entity.DocumentAttributeName;
import at.procon.dip.domain.document.repository.DocumentAttributeNameRepository;
import at.procon.dip.domain.document.repository.DocumentAttributeRepository;
import at.procon.dip.ingestion.dto.GenericNameValuePairRequest;
import at.procon.ted.util.HashUtils;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

@Service
@RequiredArgsConstructor
@Transactional
public class DocumentAttributeService {

    public static final String GLOBAL_CONTEXT = "GLOBAL";

    private final DocumentService documentService;
    private final DocumentAttributeNameRepository attributeNameRepository;
    private final DocumentAttributeRepository attributeRepository;

    public void addAttributes(UUID documentId, List<GenericNameValuePairRequest> attributes) {
        if (attributes == null || attributes.isEmpty()) {
            return;
        }

        Document document = documentService.getRequired(documentId);
        Set<String> seenInRequest = new HashSet<>();
        Map<String, DocumentAttributeName> catalogEntriesByKey = new HashMap<>();

        for (GenericNameValuePairRequest attribute : attributes) {
            ResolvedAttributeInput resolved = resolveInput(attribute);
            if (resolved == null) {
                continue;
            }

            String catalogKey = resolved.normalizedName() + "|" + resolved.context();
            DocumentAttributeName catalogEntry = catalogEntriesByKey.computeIfAbsent(catalogKey, ignored ->
                    resolveOrCreateCatalogEntry(resolved));

            if (catalogEntry.getAttributeValueType() != resolved.type()) {
                throw new IllegalArgumentException(
                        "Attribute '" + resolved.name() + "' in context '" + resolved.context() + "' is already registered as type "
                                + catalogEntry.getAttributeValueType() + " but request uses " + resolved.type()
                );
            }

            String attributeValueHash = HashUtils.computeSha256(resolved.canonicalValue());
            String requestKey = catalogEntry.getId() + ":" + attributeValueHash;
            if (!seenInRequest.add(requestKey)) {
                continue;
            }

            boolean alreadyAssigned = attributeRepository.existsByDocument_IdAndAttributeName_IdAndAttributeValueHash(
                    document.getId(),
                    catalogEntry.getId(),
                    attributeValueHash
            );
            if (alreadyAssigned) {
                continue;
            }

            DocumentAttribute entity = DocumentAttribute.builder()
                    .document(document)
                    .attributeName(catalogEntry)
                    .attributeValueHash(attributeValueHash)
                    .build();
            applyTypedValue(entity, resolved);
            attributeRepository.save(entity);
        }
    }

    private DocumentAttributeName resolveOrCreateCatalogEntry(ResolvedAttributeInput resolved) {
        return attributeNameRepository.findByNormalizedNameAndAttributeContext(resolved.normalizedName(), resolved.context())
                .orElseGet(() -> attributeNameRepository.save(DocumentAttributeName.builder()
                        .attributeName(resolved.name())
                        .normalizedName(resolved.normalizedName())
                        .attributeContext(resolved.context())
                        .attributeValueType(resolved.type())
                        .build()));
    }

    private void applyTypedValue(DocumentAttribute entity, ResolvedAttributeInput resolved) {
        switch (resolved.type()) {
            case STRING -> entity.setStringValue(resolved.stringValue());
            case INTEGER -> entity.setIntegerValue(resolved.integerValue());
            case NUMBER -> entity.setNumberValue(resolved.numberValue());
            case DATE -> entity.setDateValue(resolved.dateValue());
            case DATETIME -> entity.setDatetimeValue(resolved.datetimeValue());
            case BOOLEAN -> entity.setBooleanValue(resolved.booleanValue());
        }
    }

    private ResolvedAttributeInput resolveInput(GenericNameValuePairRequest attribute) {
        if (attribute == null || !StringUtils.hasText(attribute.name())) {
            return null;
        }

        String name = attribute.name().trim();
        String normalizedName = normalizeName(name);
        String context = normalizeContext(attribute.context());

        int explicitTypedValues = countExplicitTypedValues(attribute);
        String legacyValue = StringUtils.hasText(attribute.value()) ? attribute.value().trim() : null;
        String explicitStringValue = StringUtils.hasText(attribute.stringValue()) ? attribute.stringValue().trim() : null;
        DocumentAttributeValueType requestedType = attribute.type();

        if (requestedType == null) {
            if (legacyValue != null && explicitTypedValues > 0) {
                throw new IllegalArgumentException("Attribute '" + name + "' uses both legacy and typed value fields");
            }
            if (explicitTypedValues > 1) {
                throw new IllegalArgumentException("Attribute '" + name + "' defines multiple typed values without declaring a type");
            }
            if (legacyValue != null) {
                return new ResolvedAttributeInput(
                        name, normalizedName, context, DocumentAttributeValueType.STRING, legacyValue, null, null, null, null, null, legacyValue
                );
            }
            if (explicitStringValue != null && explicitTypedValues == 1) {
                return new ResolvedAttributeInput(
                        name, normalizedName, context, DocumentAttributeValueType.STRING, explicitStringValue, null, null, null, null, null, explicitStringValue
                );
            }
            if (attribute.integerValue() != null && explicitTypedValues == 1) {
                return new ResolvedAttributeInput(
                        name, normalizedName, context, DocumentAttributeValueType.INTEGER, null, attribute.integerValue(), null, null, null, null, attribute.integerValue().toString()
                );
            }
            if (attribute.numberValue() != null && explicitTypedValues == 1) {
                BigDecimal normalized = normalizeNumber(attribute.numberValue());
                return new ResolvedAttributeInput(
                        name, normalizedName, context, DocumentAttributeValueType.NUMBER, null, null, normalized, null, null, null, normalized.toPlainString()
                );
            }
            if (attribute.dateValue() != null && explicitTypedValues == 1) {
                return new ResolvedAttributeInput(
                        name, normalizedName, context, DocumentAttributeValueType.DATE, null, null, null, attribute.dateValue(), null, null, attribute.dateValue().toString()
                );
            }
            if (attribute.datetimeValue() != null && explicitTypedValues == 1) {
                OffsetDateTime normalized = normalizeDateTime(attribute.datetimeValue());
                return new ResolvedAttributeInput(
                        name, normalizedName, context, DocumentAttributeValueType.DATETIME, null, null, null, null, normalized, null, normalized.toString()
                );
            }
            if (attribute.booleanValue() != null && explicitTypedValues == 1) {
                return new ResolvedAttributeInput(
                        name, normalizedName, context, DocumentAttributeValueType.BOOLEAN, null, null, null, null, null, attribute.booleanValue(), attribute.booleanValue().toString()
                );
            }
            return null;
        }

        return switch (requestedType) {
            case STRING -> {
                String value = explicitStringValue != null ? explicitStringValue : legacyValue;
                if (!StringUtils.hasText(value) || hasOtherTypedValues(attribute, DocumentAttributeValueType.STRING)) {
                    throw new IllegalArgumentException("String attribute '" + name + "' requires only string or value input");
                }
                yield new ResolvedAttributeInput(
                        name, normalizedName, context, DocumentAttributeValueType.STRING, value.trim(), null, null, null, null, null, value.trim()
                );
            }
            case INTEGER -> {
                Long value = attribute.integerValue();
                if (value == null && legacyValue != null) {
                    value = Long.parseLong(legacyValue);
                }
                if (value == null || hasOtherTypedValues(attribute, DocumentAttributeValueType.INTEGER)) {
                    throw new IllegalArgumentException("Integer attribute '" + name + "' requires only integer input");
                }
                yield new ResolvedAttributeInput(
                        name, normalizedName, context, DocumentAttributeValueType.INTEGER, null, value, null, null, null, null, value.toString()
                );
            }
            case NUMBER -> {
                BigDecimal value = attribute.numberValue();
                if (value == null && legacyValue != null) {
                    value = normalizeNumber(new BigDecimal(legacyValue));
                }
                if (value == null || hasOtherTypedValues(attribute, DocumentAttributeValueType.NUMBER)) {
                    throw new IllegalArgumentException("Number attribute '" + name + "' requires only number input");
                }
                value = normalizeNumber(value);
                yield new ResolvedAttributeInput(
                        name, normalizedName, context, DocumentAttributeValueType.NUMBER, null, null, value, null, null, null, value.toPlainString()
                );
            }
            case DATE -> {
                LocalDate value = attribute.dateValue();
                if (value == null && legacyValue != null) {
                    value = LocalDate.parse(legacyValue);
                }
                if (value == null || hasOtherTypedValues(attribute, DocumentAttributeValueType.DATE)) {
                    throw new IllegalArgumentException("Date attribute '" + name + "' requires only date input");
                }
                yield new ResolvedAttributeInput(
                        name, normalizedName, context, DocumentAttributeValueType.DATE, null, null, null, value, null, null, value.toString()
                );
            }
            case DATETIME -> {
                OffsetDateTime value = attribute.datetimeValue();
                if (value == null && legacyValue != null) {
                    value = OffsetDateTime.parse(legacyValue);
                }
                if (value == null || hasOtherTypedValues(attribute, DocumentAttributeValueType.DATETIME)) {
                    throw new IllegalArgumentException("Datetime attribute '" + name + "' requires only datetime input");
                }
                value = normalizeDateTime(value);
                yield new ResolvedAttributeInput(
                        name, normalizedName, context, DocumentAttributeValueType.DATETIME, null, null, null, null, value, null, value.toString()
                );
            }
            case BOOLEAN -> {
                Boolean value = attribute.booleanValue();
                if (value == null && legacyValue != null) {
                    if ("true".equalsIgnoreCase(legacyValue) || "false".equalsIgnoreCase(legacyValue)) {
                        value = Boolean.parseBoolean(legacyValue);
                    } else {
                        throw new IllegalArgumentException("Boolean attribute '" + name + "' requires true or false");
                    }
                }
                if (value == null || hasOtherTypedValues(attribute, DocumentAttributeValueType.BOOLEAN)) {
                    throw new IllegalArgumentException("Boolean attribute '" + name + "' requires only boolean input");
                }
                yield new ResolvedAttributeInput(
                        name, normalizedName, context, DocumentAttributeValueType.BOOLEAN, null, null, null, null, null, value, value.toString()
                );
            }
        };
    }

    private int countExplicitTypedValues(GenericNameValuePairRequest attribute) {
        int count = 0;
        if (StringUtils.hasText(attribute.stringValue())) {
            count++;
        }
        if (attribute.integerValue() != null) {
            count++;
        }
        if (attribute.numberValue() != null) {
            count++;
        }
        if (attribute.dateValue() != null) {
            count++;
        }
        if (attribute.datetimeValue() != null) {
            count++;
        }
        if (attribute.booleanValue() != null) {
            count++;
        }
        return count;
    }

    private boolean hasOtherTypedValues(GenericNameValuePairRequest attribute, DocumentAttributeValueType expectedType) {
        return switch (expectedType) {
            case STRING -> attribute.integerValue() != null || attribute.numberValue() != null || attribute.dateValue() != null || attribute.datetimeValue() != null || attribute.booleanValue() != null;
            case INTEGER -> StringUtils.hasText(attribute.stringValue()) || attribute.numberValue() != null || attribute.dateValue() != null || attribute.datetimeValue() != null || attribute.booleanValue() != null;
            case NUMBER -> StringUtils.hasText(attribute.stringValue()) || attribute.integerValue() != null || attribute.dateValue() != null || attribute.datetimeValue() != null || attribute.booleanValue() != null;
            case DATE -> StringUtils.hasText(attribute.stringValue()) || attribute.integerValue() != null || attribute.numberValue() != null || attribute.datetimeValue() != null || attribute.booleanValue() != null;
            case DATETIME -> StringUtils.hasText(attribute.stringValue()) || attribute.integerValue() != null || attribute.numberValue() != null || attribute.dateValue() != null || attribute.booleanValue() != null;
            case BOOLEAN -> StringUtils.hasText(attribute.stringValue()) || attribute.integerValue() != null || attribute.numberValue() != null || attribute.dateValue() != null || attribute.datetimeValue() != null;
        };
    }

    private BigDecimal normalizeNumber(BigDecimal value) {
        return value.stripTrailingZeros();
    }

    private OffsetDateTime normalizeDateTime(OffsetDateTime value) {
        return value.withNano(0);
    }

    private String normalizeName(String value) {
        return value.trim().toLowerCase(Locale.ROOT);
    }

    private String normalizeContext(String value) {
        return StringUtils.hasText(value)
                ? value.trim().toUpperCase(Locale.ROOT)
                : GLOBAL_CONTEXT;
    }

    private record ResolvedAttributeInput(
            String name,
            String normalizedName,
            String context,
            DocumentAttributeValueType type,
            String stringValue,
            Long integerValue,
            BigDecimal numberValue,
            LocalDate dateValue,
            OffsetDateTime datetimeValue,
            Boolean booleanValue,
            String canonicalValue
    ) {
    }
}
