package com.cameleer.server.app.storage;

import com.cameleer.common.graph.RouteGraph;
import com.cameleer.common.graph.RouteNode;
import com.cameleer.server.core.ingestion.TaggedDiagram;
import com.cameleer.server.core.storage.DiagramStore;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.HashMap;
import java.util.HexFormat;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

/**
 * ClickHouse implementation of {@link DiagramStore}.
 * <p>
 * Stores route graphs as JSON with SHA-256 content-hash deduplication.
 * Uses ReplacingMergeTree — duplicate inserts are deduplicated on merge.
 * <p>
 * In-memory caches avoid expensive ClickHouse scans for the hot lookup paths
 * (route+instance → hash, hash → graph). Diagrams change rarely (only on
 * route topology changes), so the caches are effectively append-only.
 */
public class ClickHouseDiagramStore implements DiagramStore {

    private static final Logger log = LoggerFactory.getLogger(ClickHouseDiagramStore.class);

    private static final String INSERT_SQL = """
            INSERT INTO route_diagrams
                (tenant_id, content_hash, route_id, instance_id, application_id, environment, definition, created_at)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?)
            """;

    private static final String SELECT_BY_HASH = """
            SELECT definition FROM route_diagrams
            WHERE tenant_id = ? AND content_hash = ?
            LIMIT 1
            """;

    private static final String SELECT_HASH_FOR_ROUTE = """
            SELECT content_hash FROM route_diagrams
            WHERE tenant_id = ? AND route_id = ? AND instance_id = ?
            ORDER BY created_at DESC LIMIT 1
            """;

    private static final String SELECT_HASH_FOR_APP_ROUTE = """
            SELECT content_hash FROM route_diagrams
            WHERE tenant_id = ? AND application_id = ? AND environment = ? AND route_id = ?
            ORDER BY created_at DESC LIMIT 1
            """;

    private static final String SELECT_DEFINITIONS_FOR_APP = """
            SELECT DISTINCT route_id, definition FROM route_diagrams
            WHERE tenant_id = ? AND application_id = ? AND environment = ?
            """;

    private final String tenantId;
    private final JdbcTemplate jdbc;
    private final ObjectMapper objectMapper;

    // (routeId + "\0" + instanceId) → contentHash
    private final ConcurrentHashMap<String, String> hashCache = new ConcurrentHashMap<>();
    // (applicationId + "\0" + environment + "\0" + routeId) → most recent contentHash
    private final ConcurrentHashMap<String, String> appRouteHashCache = new ConcurrentHashMap<>();
    // contentHash → deserialized RouteGraph
    private final ConcurrentHashMap<String, RouteGraph> graphCache = new ConcurrentHashMap<>();

    public ClickHouseDiagramStore(String tenantId, JdbcTemplate jdbc) {
        this.tenantId = tenantId;
        this.jdbc = jdbc;
        this.objectMapper = new ObjectMapper();
        this.objectMapper.registerModule(new JavaTimeModule());
        warmLoadHashCache();
    }

    private void warmLoadHashCache() {
        try {
            jdbc.query(
                    "SELECT route_id, instance_id, content_hash FROM route_diagrams WHERE tenant_id = ?",
                    rs -> {
                        String key = rs.getString("route_id") + "\0" + rs.getString("instance_id");
                        hashCache.put(key, rs.getString("content_hash"));
                    },
                    tenantId);
            log.info("Diagram hash cache warmed: {} entries", hashCache.size());
        } catch (Exception e) {
            log.warn("Failed to warm diagram hash cache — lookups will fall back to ClickHouse: {}", e.getMessage());
        }

        try {
            jdbc.query(
                    "SELECT application_id, environment, route_id, " +
                            "argMax(content_hash, created_at) AS content_hash " +
                            "FROM route_diagrams WHERE tenant_id = ? " +
                            "GROUP BY application_id, environment, route_id",
                    rs -> {
                        String key = appRouteCacheKey(
                                rs.getString("application_id"),
                                rs.getString("environment"),
                                rs.getString("route_id"));
                        appRouteHashCache.put(key, rs.getString("content_hash"));
                    },
                    tenantId);
            log.info("Diagram app-route cache warmed: {} entries", appRouteHashCache.size());
        } catch (Exception e) {
            log.warn("Failed to warm diagram app-route cache — lookups will fall back to ClickHouse: {}", e.getMessage());
        }
    }

    private static String cacheKey(String routeId, String instanceId) {
        return routeId + "\0" + instanceId;
    }

    private static String appRouteCacheKey(String applicationId, String environment, String routeId) {
        return (applicationId != null ? applicationId : "") + "\0"
                + (environment != null ? environment : "") + "\0"
                + (routeId != null ? routeId : "");
    }

    @Override
    public void store(TaggedDiagram diagram) {
        try {
            RouteGraph graph = diagram.graph();
            String agentId = diagram.instanceId() != null ? diagram.instanceId() : "";
            String applicationId = diagram.applicationId() != null ? diagram.applicationId() : "";
            String environment = (diagram.environment() != null && !diagram.environment().isBlank())
                    ? diagram.environment() : "default";
            String json = objectMapper.writeValueAsString(graph);
            String contentHash = sha256Hex(json);
            String routeId = graph.getRouteId() != null ? graph.getRouteId() : "";

            jdbc.update(INSERT_SQL,
                    tenantId,
                    contentHash,
                    routeId,
                    agentId,
                    applicationId,
                    environment,
                    json,
                    Timestamp.from(Instant.now()));

            // Update caches
            hashCache.put(cacheKey(routeId, agentId), contentHash);
            appRouteHashCache.put(appRouteCacheKey(applicationId, environment, routeId), contentHash);
            graphCache.put(contentHash, graph);

            log.debug("Stored diagram for route={} agent={} with hash={}", routeId, agentId, contentHash);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("Failed to serialize RouteGraph to JSON", e);
        }
    }

    @Override
    public Optional<RouteGraph> findByContentHash(String contentHash) {
        RouteGraph cached = graphCache.get(contentHash);
        if (cached != null) {
            return Optional.of(cached);
        }

        List<Map<String, Object>> rows = jdbc.queryForList(SELECT_BY_HASH, tenantId, contentHash);
        if (rows.isEmpty()) {
            return Optional.empty();
        }
        String json = (String) rows.get(0).get("definition");
        try {
            RouteGraph graph = objectMapper.readValue(json, RouteGraph.class);
            graphCache.put(contentHash, graph);
            return Optional.of(graph);
        } catch (JsonProcessingException e) {
            log.error("Failed to deserialize RouteGraph from ClickHouse", e);
            return Optional.empty();
        }
    }

    @Override
    public Optional<String> findContentHashForRoute(String routeId, String agentId) {
        String cached = hashCache.get(cacheKey(routeId, agentId));
        if (cached != null) {
            return Optional.of(cached);
        }

        List<Map<String, Object>> rows = jdbc.queryForList(
                SELECT_HASH_FOR_ROUTE, tenantId, routeId, agentId);
        if (rows.isEmpty()) {
            return Optional.empty();
        }
        String hash = (String) rows.get(0).get("content_hash");
        hashCache.put(cacheKey(routeId, agentId), hash);
        return Optional.of(hash);
    }

    @Override
    public Optional<String> findLatestContentHashForAppRoute(String applicationId,
                                                             String routeId,
                                                             String environment) {
        if (applicationId == null || applicationId.isBlank()
                || routeId == null || routeId.isBlank()
                || environment == null || environment.isBlank()) {
            return Optional.empty();
        }

        String key = appRouteCacheKey(applicationId, environment, routeId);
        String cached = appRouteHashCache.get(key);
        if (cached != null) {
            return Optional.of(cached);
        }

        List<Map<String, Object>> rows = jdbc.queryForList(
                SELECT_HASH_FOR_APP_ROUTE, tenantId, applicationId, environment, routeId);
        if (rows.isEmpty()) {
            return Optional.empty();
        }
        String hash = (String) rows.get(0).get("content_hash");
        appRouteHashCache.put(key, hash);
        return Optional.of(hash);
    }

    @Override
    public Map<String, String> findProcessorRouteMapping(String applicationId, String environment) {
        Map<String, String> mapping = new HashMap<>();
        List<Map<String, Object>> rows = jdbc.queryForList(
                SELECT_DEFINITIONS_FOR_APP, tenantId, applicationId, environment);
        for (Map<String, Object> row : rows) {
            String routeId = (String) row.get("route_id");
            String json = (String) row.get("definition");
            if (json == null || routeId == null) {
                continue;
            }
            try {
                RouteGraph graph = objectMapper.readValue(json, RouteGraph.class);
                collectNodeIds(graph.getRoot(), routeId, mapping);
            } catch (JsonProcessingException e) {
                log.warn("Failed to deserialize RouteGraph for route={} app={} env={}",
                        routeId, applicationId, environment, e);
            }
        }
        return mapping;
    }

    private void collectNodeIds(RouteNode node, String routeId, Map<String, String> mapping) {
        if (node == null) {
            return;
        }
        String id = node.getId();
        if (id != null && !id.isEmpty()) {
            mapping.put(id, routeId);
        }
        List<RouteNode> children = node.getChildren();
        if (children != null) {
            for (RouteNode child : children) {
                collectNodeIds(child, routeId, mapping);
            }
        }
    }

    static String sha256Hex(String input) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
            return HexFormat.of().formatHex(hash);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("SHA-256 not available", e);
        }
    }
}
