package at.procon.eventhub.persistence;

import at.procon.eventhub.importing.masterdata.SourceMasterEntityUpsert;
import at.procon.eventhub.importing.masterdata.SourceMasterRelationUpsert;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.POJONode;
import java.lang.reflect.Array;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.OffsetDateTime;
import java.time.temporal.TemporalAccessor;
import java.time.temporal.TemporalAmount;
import java.util.ArrayList;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

@Repository
public class SourceMasterDataRepository {

    private final JdbcTemplate jdbcTemplate;
    private final ObjectMapper objectMapper;

    public SourceMasterDataRepository(JdbcTemplate jdbcTemplate, ObjectMapper objectMapper) {
        this.jdbcTemplate = jdbcTemplate;
        this.objectMapper = objectMapper;
    }

    @Transactional
    public int upsertEntities(String tenantKey, int eventSourceId, List<SourceMasterEntityUpsert> entities) {
        List<SourceMasterEntityStageRow> rows = new ArrayList<>(entities.size());
        for (SourceMasterEntityUpsert entity : entities) {
            if (entity.sourceEntityId() == null || entity.sourceEntityId().isBlank()) {
                continue;
            }
            rows.add(new SourceMasterEntityStageRow(
                    UUID.randomUUID(),
                    entity.entityType(),
                    entity.sourceEntityId(),
                    entity.sourceExternalKey(),
                    entity.displayName(),
                    entity.active(),
                    entity.validFrom(),
                    entity.validTo(),
                    entity.sourceUpdatedAt(),
                    toJson(entity.payload())
            ));
        }
        if (rows.isEmpty()) {
            return 0;
        }

        createEntityStage();
        stageEntities(rows);
        upsertStagedEntities(tenantKey, eventSourceId);
        return rows.size();
    }

    @Transactional
    public int upsertRelations(String tenantKey, int eventSourceId, List<SourceMasterRelationUpsert> relations) {
        List<SourceMasterRelationStageRow> rows = new ArrayList<>(relations.size());
        for (SourceMasterRelationUpsert relation : relations) {
            if (relation.fromSourceEntityId() == null || relation.toSourceEntityId() == null) {
                continue;
            }
            rows.add(new SourceMasterRelationStageRow(
                    UUID.randomUUID(),
                    relationKey(relation),
                    relation.relationType(),
                    relation.fromEntityType(),
                    relation.fromSourceEntityId(),
                    relation.toEntityType(),
                    relation.toSourceEntityId(),
                    relation.validFrom(),
                    relation.validTo(),
                    relation.sourceUpdatedAt(),
                    toJson(relation.payload())
            ));
        }
        if (rows.isEmpty()) {
            return 0;
        }

        createRelationStage();
        stageRelations(rows);
        upsertStagedRelations(tenantKey, eventSourceId);
        return rows.size();
    }

    private void createEntityStage() {
        jdbcTemplate.execute(
                """
                create temporary table if not exists eventhub_source_master_entity_stage (
                    row_no integer not null,
                    id uuid not null,
                    entity_type text not null,
                    source_entity_id text not null,
                    source_external_key text,
                    display_name text,
                    active boolean,
                    valid_from timestamptz,
                    valid_to timestamptz,
                    source_updated_at timestamptz,
                    payload jsonb not null
                ) on commit drop
                """
        );
        jdbcTemplate.execute("truncate table eventhub_source_master_entity_stage");
    }

    private void stageEntities(List<SourceMasterEntityStageRow> rows) {
        jdbcTemplate.batchUpdate(
                """
                insert into eventhub_source_master_entity_stage(
                    row_no, id, entity_type, source_entity_id, source_external_key, display_name,
                    active, valid_from, valid_to, source_updated_at, payload
                ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?::jsonb)
                """,
                new BatchPreparedStatementSetter() {
                    @Override
                    public void setValues(PreparedStatement ps, int i) throws SQLException {
                        SourceMasterEntityStageRow row = rows.get(i);
                        ps.setInt(1, i);
                        ps.setObject(2, row.id());
                        ps.setString(3, row.entityType());
                        ps.setString(4, row.sourceEntityId());
                        ps.setString(5, row.sourceExternalKey());
                        ps.setString(6, row.displayName());
                        ps.setObject(7, row.active());
                        ps.setObject(8, row.validFrom());
                        ps.setObject(9, row.validTo());
                        ps.setObject(10, row.sourceUpdatedAt());
                        ps.setString(11, row.payloadJson());
                    }

                    @Override
                    public int getBatchSize() {
                        return rows.size();
                    }
                }
        );
    }

    private void upsertStagedEntities(String tenantKey, int eventSourceId) {
        jdbcTemplate.update(
                """
                insert into eventhub.source_master_entity(
                    id, tenant_key, event_source_id, entity_type, source_entity_id,
                    source_external_key, display_name, active, valid_from, valid_to,
                    source_updated_at, payload, updated_at
                )
                select
                    id, ?, ?, entity_type, source_entity_id,
                    source_external_key, display_name, active, valid_from, valid_to,
                    source_updated_at, payload, now()
                from (
                    select distinct on (entity_type, source_entity_id) *
                    from eventhub_source_master_entity_stage
                    order by entity_type, source_entity_id, row_no desc
                ) stage
                on conflict (tenant_key, event_source_id, entity_type, source_entity_id)
                do update set
                    source_external_key = excluded.source_external_key,
                    display_name = excluded.display_name,
                    active = excluded.active,
                    valid_from = excluded.valid_from,
                    valid_to = excluded.valid_to,
                    source_updated_at = excluded.source_updated_at,
                    payload = excluded.payload,
                    updated_at = now()
                """,
                tenantKey,
                eventSourceId
        );
    }

    private void createRelationStage() {
        jdbcTemplate.execute(
                """
                create temporary table if not exists eventhub_source_master_relation_stage (
                    row_no integer not null,
                    id uuid not null,
                    relation_key text not null,
                    relation_type text not null,
                    from_entity_type text not null,
                    from_source_entity_id text not null,
                    to_entity_type text not null,
                    to_source_entity_id text not null,
                    valid_from timestamptz,
                    valid_to timestamptz,
                    source_updated_at timestamptz,
                    payload jsonb not null
                ) on commit drop
                """
        );
        jdbcTemplate.execute("truncate table eventhub_source_master_relation_stage");
    }

    private void stageRelations(List<SourceMasterRelationStageRow> rows) {
        jdbcTemplate.batchUpdate(
                """
                insert into eventhub_source_master_relation_stage(
                    row_no, id, relation_key, relation_type, from_entity_type, from_source_entity_id,
                    to_entity_type, to_source_entity_id, valid_from, valid_to, source_updated_at, payload
                ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?::jsonb)
                """,
                new BatchPreparedStatementSetter() {
                    @Override
                    public void setValues(PreparedStatement ps, int i) throws SQLException {
                        SourceMasterRelationStageRow row = rows.get(i);
                        ps.setInt(1, i);
                        ps.setObject(2, row.id());
                        ps.setString(3, row.relationKey());
                        ps.setString(4, row.relationType());
                        ps.setString(5, row.fromEntityType());
                        ps.setString(6, row.fromSourceEntityId());
                        ps.setString(7, row.toEntityType());
                        ps.setString(8, row.toSourceEntityId());
                        ps.setObject(9, row.validFrom());
                        ps.setObject(10, row.validTo());
                        ps.setObject(11, row.sourceUpdatedAt());
                        ps.setString(12, row.payloadJson());
                    }

                    @Override
                    public int getBatchSize() {
                        return rows.size();
                    }
                }
        );
    }

    private void upsertStagedRelations(String tenantKey, int eventSourceId) {
        jdbcTemplate.update(
                """
                insert into eventhub.source_master_relation(
                    id, tenant_key, event_source_id, relation_key, relation_type,
                    from_entity_type, from_source_entity_id, to_entity_type, to_source_entity_id,
                    valid_from, valid_to, source_updated_at, payload, updated_at
                )
                select
                    id, ?, ?, relation_key, relation_type,
                    from_entity_type, from_source_entity_id, to_entity_type, to_source_entity_id,
                    valid_from, valid_to, source_updated_at, payload, now()
                from (
                    select distinct on (relation_key) *
                    from eventhub_source_master_relation_stage
                    order by relation_key, row_no desc
                ) stage
                on conflict (tenant_key, event_source_id, relation_key)
                do update set
                    relation_type = excluded.relation_type,
                    from_entity_type = excluded.from_entity_type,
                    from_source_entity_id = excluded.from_source_entity_id,
                    to_entity_type = excluded.to_entity_type,
                    to_source_entity_id = excluded.to_source_entity_id,
                    valid_from = excluded.valid_from,
                    valid_to = excluded.valid_to,
                    source_updated_at = excluded.source_updated_at,
                    payload = excluded.payload,
                    updated_at = now()
                """,
                tenantKey,
                eventSourceId
        );
    }

    public UUID resolveOrCreateEntityId(
            String tenantKey,
            int eventSourceId,
            String entityType,
            String sourceEntityId,
            String sourceExternalKey,
            String displayName,
            Boolean active,
            Map<String, Object> payload
    ) {
        String normalizedTenantKey = normalizeRequired(tenantKey, "tenantKey");
        String normalizedEntityType = normalizeRequired(entityType, "entityType").toUpperCase();
        String normalizedSourceEntityId = normalizeRequired(sourceEntityId, "sourceEntityId");
        String normalizedSourceExternalKey = normalizeNullable(sourceExternalKey);
        String normalizedDisplayName = normalizeNullable(displayName);
        UUID existing = findEntityId(normalizedTenantKey, eventSourceId, normalizedEntityType, normalizedSourceEntityId);
        if (existing != null) {
            return existing;
        }

        UUID created = jdbcTemplate.query(
                con -> {
                    PreparedStatement ps = con.prepareStatement(
                            """
                            insert into eventhub.source_master_entity(
                                id, tenant_key, event_source_id, entity_type, source_entity_id,
                                source_external_key, display_name, active, payload, updated_at
                            ) values (?, ?, ?, ?, ?, ?, ?, ?, ?::jsonb, now())
                            on conflict (tenant_key, event_source_id, entity_type, source_entity_id) do nothing
                            returning id
                            """
                    );
                    ps.setObject(1, UUID.randomUUID());
                    ps.setString(2, normalizedTenantKey);
                    ps.setInt(3, eventSourceId);
                    ps.setString(4, normalizedEntityType);
                    ps.setString(5, normalizedSourceEntityId);
                    ps.setString(6, normalizedSourceExternalKey);
                    ps.setString(7, normalizedDisplayName);
                    ps.setObject(8, active);
                    ps.setString(9, toJson(payload));
                    return ps;
                },
                rs -> {
                    if (!rs.next()) {
                        return null;
                    }
                    return (UUID) rs.getObject(1);
                }
        );
        if (created != null) {
            return created;
        }

        UUID resolved = findEntityId(normalizedTenantKey, eventSourceId, normalizedEntityType, normalizedSourceEntityId);
        if (resolved != null) {
            return resolved;
        }
        throw new IllegalStateException(
                "Could not resolve source master entity id for "
                        + normalizedTenantKey + ":" + eventSourceId + ":" + normalizedEntityType + ":" + normalizedSourceEntityId
        );
    }

    private UUID findEntityId(String tenantKey, int eventSourceId, String entityType, String sourceEntityId) {
        return jdbcTemplate.query(
                """
                select id
                from eventhub.source_master_entity
                where tenant_key = ?
                  and event_source_id = ?
                  and entity_type = ?
                  and source_entity_id = ?
                """,
                rs -> rs.next() ? (UUID) rs.getObject("id") : null,
                tenantKey,
                eventSourceId,
                entityType,
                sourceEntityId
        );
    }

    private String relationKey(SourceMasterRelationUpsert relation) {
        String validFrom = relation.validFrom() == null ? "MIN" : relation.validFrom().toString();
        String validTo = relation.validTo() == null ? "MAX" : relation.validTo().toString();
        return relation.relationType()
                + ":" + relation.fromEntityType()
                + ":" + relation.fromSourceEntityId()
                + ":" + relation.toEntityType()
                + ":" + relation.toSourceEntityId()
                + ":" + validFrom
                + ":" + validTo;
    }

    private String toJson(Map<String, Object> value) {
        Map<String, Object> source = value == null ? Map.of() : value;
        try {
            return objectMapper.writeValueAsString(normalizeJsonMap(source));
        } catch (JsonProcessingException e) {
            throw new IllegalArgumentException(
                    "Cannot serialize source master data payload. payloadTypes=" + describePayloadTypes(source),
                    e
            );
        }
    }

    private Map<String, Object> normalizeJsonMap(Map<?, ?> source) {
        Map<String, Object> normalized = new LinkedHashMap<>(source.size());
        for (Map.Entry<?, ?> entry : source.entrySet()) {
            if (entry.getKey() == null) {
                continue;
            }
            normalized.put(String.valueOf(entry.getKey()), normalizeJsonValue(entry.getValue()));
        }
        return normalized;
    }

    private Object normalizeJsonValue(Object value) {
        if (value == null
                || value instanceof String
                || value instanceof Number
                || value instanceof Boolean) {
            return value;
        }
        if (value instanceof JsonNode node) {
            return normalizeJsonNode(node);
        }
        if (value instanceof Enum<?> enumValue) {
            return enumValue.name();
        }
        if (value instanceof UUID uuid) {
            return uuid.toString();
        }
        if (value instanceof byte[] bytes) {
            return Base64.getEncoder().encodeToString(bytes);
        }
        if (value instanceof Timestamp timestamp) {
            return timestamp.toInstant().toString();
        }
        if (value instanceof java.util.Date date) {
            return date.toInstant().toString();
        }
        if (value instanceof TemporalAccessor || value instanceof TemporalAmount) {
            return value.toString();
        }
        if (value instanceof Map<?, ?> map) {
            return normalizeJsonMap(map);
        }
        if (value instanceof Iterable<?> iterable) {
            List<Object> values = new ArrayList<>();
            for (Object entry : iterable) {
                values.add(normalizeJsonValue(entry));
            }
            return values;
        }
        if (value.getClass().isArray()) {
            int length = Array.getLength(value);
            List<Object> values = new ArrayList<>(length);
            for (int i = 0; i < length; i++) {
                values.add(normalizeJsonValue(Array.get(value, i)));
            }
            return values;
        }
        return String.valueOf(value);
    }

    private Object normalizeJsonNode(JsonNode node) {
        if (node == null || node.isNull() || node.isMissingNode()) {
            return null;
        }
        if (node instanceof POJONode pojoNode) {
            return normalizeJsonValue(pojoNode.getPojo());
        }
        if (node.isObject()) {
            return normalizeJsonMap(objectNodeToMap(node));
        }
        if (node.isArray()) {
            List<Object> values = new ArrayList<>();
            for (JsonNode child : node) {
                values.add(normalizeJsonNode(child));
            }
            return values;
        }
        if (node.isTextual()) {
            return node.textValue();
        }
        if (node.isNumber()) {
            return node.numberValue();
        }
        if (node.isBoolean()) {
            return node.booleanValue();
        }
        if (node.isBinary()) {
            return node.asText();
        }
        return node.asText();
    }

    private Map<String, Object> objectNodeToMap(JsonNode node) {
        Map<String, Object> values = new LinkedHashMap<>();
        node.fields().forEachRemaining(entry -> values.put(entry.getKey(), entry.getValue()));
        return values;
    }

    private String describePayloadTypes(Map<String, Object> payload) {
        if (payload.isEmpty()) {
            return "{}";
        }
        return payload.entrySet()
                .stream()
                .map(entry -> entry.getKey() + "=" + (entry.getValue() == null ? "null" : entry.getValue().getClass().getName()))
                .collect(Collectors.joining(", ", "{", "}"));
    }

    private String normalizeRequired(String value, String fieldName) {
        String normalized = normalizeNullable(value);
        if (normalized == null) {
            throw new IllegalArgumentException(fieldName + " must not be blank");
        }
        return normalized;
    }

    private String normalizeNullable(String value) {
        if (value == null) {
            return null;
        }
        String trimmed = value.trim();
        return trimmed.isEmpty() ? null : trimmed;
    }

    private record SourceMasterEntityStageRow(
            UUID id,
            String entityType,
            String sourceEntityId,
            String sourceExternalKey,
            String displayName,
            Boolean active,
            OffsetDateTime validFrom,
            OffsetDateTime validTo,
            OffsetDateTime sourceUpdatedAt,
            String payloadJson
    ) {
    }

    private record SourceMasterRelationStageRow(
            UUID id,
            String relationKey,
            String relationType,
            String fromEntityType,
            String fromSourceEntityId,
            String toEntityType,
            String toSourceEntityId,
            OffsetDateTime validFrom,
            OffsetDateTime validTo,
            OffsetDateTime sourceUpdatedAt,
            String payloadJson
    ) {
    }
}
