package com.cameleer.server.app.controller;

import com.cameleer.server.app.config.TenantProperties;
import com.cameleer.server.app.dto.AgentSummary;
import com.cameleer.server.app.dto.CatalogApp;
import com.cameleer.server.app.dto.RouteSummary;
import com.cameleer.common.graph.RouteGraph;
import com.cameleer.server.core.agent.AgentInfo;
import com.cameleer.server.core.agent.AgentRegistryService;
import com.cameleer.server.core.agent.AgentState;
import com.cameleer.server.core.agent.RouteStateRegistry;
import com.cameleer.server.core.runtime.*;
import com.cameleer.server.core.storage.DiagramStore;
import com.cameleer.server.core.storage.RouteCatalogEntry;
import com.cameleer.server.core.storage.RouteCatalogStore;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;

import java.sql.Timestamp;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;

/**
 * Unified catalog endpoint that merges App records (PostgreSQL) with live agent data
 * and ClickHouse stats. Replaces the separate RouteCatalogController.
 */
@RestController
@RequestMapping("/api/v1/catalog")
@Tag(name = "Catalog", description = "Unified application catalog")
public class CatalogController {

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

    private final AgentRegistryService registryService;
    private final DiagramStore diagramStore;
    private final JdbcTemplate jdbc;
    private final RouteStateRegistry routeStateRegistry;
    private final AppService appService;
    private final EnvironmentService envService;
    private final DeploymentRepository deploymentRepo;
    private final TenantProperties tenantProperties;
    private final RouteCatalogStore routeCatalogStore;

    @Value("${cameleer.server.catalog.discoveryttldays:7}")
    private int discoveryTtlDays;

    public CatalogController(AgentRegistryService registryService,
                             DiagramStore diagramStore,
                             @org.springframework.beans.factory.annotation.Qualifier("clickHouseJdbcTemplate") JdbcTemplate jdbc,
                             RouteStateRegistry routeStateRegistry,
                             AppService appService,
                             EnvironmentService envService,
                             DeploymentRepository deploymentRepo,
                             TenantProperties tenantProperties,
                             RouteCatalogStore routeCatalogStore) {
        this.registryService = registryService;
        this.diagramStore = diagramStore;
        this.jdbc = jdbc;
        this.routeStateRegistry = routeStateRegistry;
        this.appService = appService;
        this.envService = envService;
        this.deploymentRepo = deploymentRepo;
        this.tenantProperties = tenantProperties;
        this.routeCatalogStore = routeCatalogStore;
    }

    @GetMapping
    @Operation(summary = "Get unified catalog",
            description = "Returns all applications (managed + unmanaged) with live agent data, routes, and deployment status")
    @ApiResponse(responseCode = "200", description = "Catalog returned")
    public ResponseEntity<List<CatalogApp>> getCatalog(
            @RequestParam(required = false) String environment,
            @RequestParam(required = false) String from,
            @RequestParam(required = false) String to) {

        // 1. Resolve environment
        Environment env = null;
        if (environment != null && !environment.isBlank()) {
            try {
                env = envService.getBySlug(environment);
            } catch (IllegalArgumentException e) {
                return ResponseEntity.ok(List.of());
            }
        }

        // 2. Get managed apps from PostgreSQL
        List<App> managedApps = env != null
                ? appService.listByEnvironment(env.id())
                : appService.listAll();
        Map<String, App> appsBySlug = managedApps.stream()
                .collect(Collectors.toMap(App::slug, a -> a, (a, b) -> a));

        // 3. Get active deployments for managed apps
        Map<UUID, Deployment> activeDeployments = new HashMap<>();
        for (App app : managedApps) {
            UUID envId = env != null ? env.id() : app.environmentId();
            deploymentRepo.findActiveByAppIdAndEnvironmentId(app.id(), envId)
                    .ifPresent(d -> activeDeployments.put(app.id(), d));
        }

        // 4. Get agents, filter by environment
        List<AgentInfo> allAgents = registryService.findAll();
        if (environment != null && !environment.isBlank()) {
            allAgents = allAgents.stream()
                    .filter(a -> environment.equals(a.environmentId()))
                    .toList();
        }
        Map<String, List<AgentInfo>> agentsByApp = allAgents.stream()
                .collect(Collectors.groupingBy(AgentInfo::applicationId, LinkedHashMap::new, Collectors.toList()));

        // 5. Collect routes per app from agents
        Map<String, Set<String>> routesByApp = new LinkedHashMap<>();
        for (var entry : agentsByApp.entrySet()) {
            Set<String> routes = new LinkedHashSet<>();
            for (AgentInfo agent : entry.getValue()) {
                if (agent.routeIds() != null) routes.addAll(agent.routeIds());
            }
            routesByApp.put(entry.getKey(), routes);
        }

        // 6. ClickHouse exchange counts
        Instant now = Instant.now();
        Instant rangeFrom = from != null ? Instant.parse(from) : now.minus(24, ChronoUnit.HOURS);
        Instant rangeTo = to != null ? Instant.parse(to) : now;
        Map<String, Long> routeExchangeCounts = new LinkedHashMap<>();
        Map<String, Instant> routeLastSeen = new LinkedHashMap<>();
        try {
            String envFilter = (environment != null && !environment.isBlank())
                    ? " AND environment = " + lit(environment) : "";
            jdbc.query(
                    "SELECT application_id, route_id, uniqMerge(total_count) AS cnt, MAX(bucket) AS last_seen " +
                            "FROM stats_1m_route WHERE bucket >= " + lit(rangeFrom) + " AND bucket < " + lit(rangeTo) +
                            envFilter + " GROUP BY application_id, route_id",
                    rs -> {
                        String key = rs.getString("application_id") + "/" + rs.getString("route_id");
                        routeExchangeCounts.put(key, rs.getLong("cnt"));
                        Timestamp ts = rs.getTimestamp("last_seen");
                        if (ts != null) routeLastSeen.put(key, ts.toInstant());
                    });
        } catch (Exception e) {
            log.warn("Failed to query route exchange counts: {}", e.getMessage());
        }

        // Merge ClickHouse routes into routesByApp
        for (var countEntry : routeExchangeCounts.entrySet()) {
            String[] parts = countEntry.getKey().split("/", 2);
            if (parts.length == 2) {
                routesByApp.computeIfAbsent(parts[0], k -> new LinkedHashSet<>()).add(parts[1]);
            }
        }

        // Merge routes from persistent catalog (covers routes with 0 executions
        // and routes from previous app versions within the selected time window)
        try {
            List<RouteCatalogEntry> catalogEntries = (environment != null && !environment.isBlank())
                    ? routeCatalogStore.findByEnvironment(environment, rangeFrom, rangeTo)
                    : routeCatalogStore.findAll(rangeFrom, rangeTo);
            for (RouteCatalogEntry entry : catalogEntries) {
                routesByApp.computeIfAbsent(entry.applicationId(), k -> new LinkedHashSet<>())
                           .add(entry.routeId());
            }
        } catch (Exception e) {
            log.warn("Failed to query route catalog: {}", e.getMessage());
        }

        // 7. Build unified catalog
        Set<String> allSlugs = new LinkedHashSet<>(appsBySlug.keySet());
        allSlugs.addAll(agentsByApp.keySet());
        allSlugs.addAll(routesByApp.keySet());

        String envSlug = env != null ? env.slug() : "";
        List<CatalogApp> catalog = new ArrayList<>();

        for (String slug : allSlugs) {
            App app = appsBySlug.get(slug);
            List<AgentInfo> agents = agentsByApp.getOrDefault(slug, List.of());

            // Auto-cleanup: skip discovered apps with no live agents and no recent data
            if (app == null && agents.isEmpty()) {
                Set<String> routes = routesByApp.getOrDefault(slug, Set.of());
                boolean hasRecentData = routes.stream().anyMatch(routeId -> {
                    Instant lastSeen = routeLastSeen.get(slug + "/" + routeId);
                    return lastSeen != null && lastSeen.isAfter(Instant.now().minus(discoveryTtlDays, ChronoUnit.DAYS));
                });
                if (!hasRecentData) continue;
            }

            Set<String> routeIds = routesByApp.getOrDefault(slug, Set.of());

            // Resolve the env slug for this row early so fromUri can survive
            // cross-env queries (env==null) against managed apps.
            String rowEnvSlug = envSlug;
            if (app != null && rowEnvSlug.isEmpty()) {
                try {
                    rowEnvSlug = envService.getById(app.environmentId()).slug();
                } catch (Exception ignored) {}
            }
            final String resolvedEnvSlug = rowEnvSlug;

            // Routes
            List<RouteSummary> routeSummaries = routeIds.stream()
                    .map(routeId -> {
                        String key = slug + "/" + routeId;
                        long count = routeExchangeCounts.getOrDefault(key, 0L);
                        Instant lastSeen = routeLastSeen.get(key);
                        String fromUri = resolveFromEndpointUri(slug, routeId, resolvedEnvSlug);
                        String state = routeStateRegistry.getState(slug, routeId).name().toLowerCase();
                        String routeState = "started".equals(state) ? null : state;
                        return new RouteSummary(routeId, count, lastSeen, fromUri, routeState);
                    })
                    .toList();

            // Agent summaries
            List<AgentSummary> agentSummaries = agents.stream()
                    .map(a -> new AgentSummary(a.instanceId(), a.displayName(), a.state().name().toLowerCase(), 0.0))
                    .toList();

            // Agent health
            String agentHealth = agents.isEmpty() ? "offline" : computeWorstHealth(agents);

            // Total exchanges
            long totalExchanges = routeSummaries.stream().mapToLong(RouteSummary::exchangeCount).sum();

            // Deployment summary (managed apps only)
            CatalogApp.DeploymentSummary deploymentSummary = null;
            DeploymentStatus deployStatus = null;
            if (app != null) {
                Deployment dep = activeDeployments.get(app.id());
                if (dep != null) {
                    deployStatus = dep.status();
                    int healthy = 0, total = 0;
                    if (dep.replicaStates() != null) {
                        total = dep.replicaStates().size();
                        healthy = (int) dep.replicaStates().stream()
                                .filter(r -> "RUNNING".equals(r.get("status")))
                                .count();
                    }
                    int version = 0;
                    try {
                        var versions = appService.listVersions(app.id());
                        version = versions.stream()
                                .filter(v -> v.id().equals(dep.appVersionId()))
                                .map(AppVersion::version)
                                .findFirst().orElse(0);
                    } catch (Exception ignored) {}

                    deploymentSummary = new CatalogApp.DeploymentSummary(
                            dep.status().name(),
                            healthy + "/" + total,
                            version
                    );
                }
            }

            // Composite health + tooltip
            String health = compositeHealth(app != null ? deployStatus : null, agentHealth);
            String healthTooltip = buildHealthTooltip(app != null, deployStatus, agentHealth, agents.size());

            String displayName = app != null ? app.displayName() : slug;

            catalog.add(new CatalogApp(
                    slug, displayName, app != null, resolvedEnvSlug,
                    health, healthTooltip, agents.size(), routeSummaries, agentSummaries,
                    totalExchanges, deploymentSummary
            ));
        }

        return ResponseEntity.ok(catalog);
    }

    private String resolveFromEndpointUri(String applicationId, String routeId, String environment) {
        if (environment == null || environment.isBlank()) {
            return null;
        }
        return diagramStore.findLatestContentHashForAppRoute(applicationId, routeId, environment)
                .flatMap(diagramStore::findByContentHash)
                .map(RouteGraph::getRoot)
                .map(root -> root.getEndpointUri())
                .orElse(null);
    }

    private static String lit(Instant instant) {
        return "'" + java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
                .withZone(java.time.ZoneOffset.UTC)
                .format(instant.truncatedTo(ChronoUnit.SECONDS)) + "'";
    }

    private static String lit(String value) {
        return "'" + value.replace("\\", "\\\\").replace("'", "\\'") + "'";
    }

    private String computeWorstHealth(List<AgentInfo> agents) {
        boolean hasDead = false;
        boolean hasStale = false;
        for (AgentInfo a : agents) {
            if (a.state() == AgentState.DEAD) hasDead = true;
            if (a.state() == AgentState.STALE) hasStale = true;
        }
        if (hasDead) return "dead";
        if (hasStale) return "stale";
        return "live";
    }

    private String compositeHealth(DeploymentStatus deployStatus, String agentHealth) {
        if (deployStatus == null) return agentHealth; // unmanaged or no deployment
        return switch (deployStatus) {
            case STARTING -> "running";
            case STOPPING, DEGRADED -> "stale";
            case STOPPED -> "dead";
            case FAILED -> "error";
            case RUNNING -> "offline".equals(agentHealth) ? "stale" : agentHealth;
        };
    }

    private String buildHealthTooltip(boolean managed, DeploymentStatus deployStatus, String agentHealth, int agentCount) {
        if (!managed) {
            return "Agents: " + agentHealth + " (" + agentCount + " connected)";
        }
        if (deployStatus == null) {
            return "No deployment";
        }
        String depPart = "Deployment: " + deployStatus.name();
        if (deployStatus == DeploymentStatus.RUNNING || deployStatus == DeploymentStatus.DEGRADED) {
            return depPart + ", Agents: " + agentHealth + " (" + agentCount + " connected)";
        }
        return depPart;
    }

    @DeleteMapping("/{applicationId}")
    @Operation(summary = "Dismiss application and purge all data")
    @ApiResponse(responseCode = "204", description = "Application dismissed")
    @ApiResponse(responseCode = "409", description = "Cannot dismiss — live agents connected")
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<Void> dismissApplication(@PathVariable String applicationId) {
        // Check for live agents
        List<AgentInfo> liveAgents = registryService.findAll().stream()
                .filter(a -> applicationId.equals(a.applicationId()))
                .filter(a -> a.state() != AgentState.DEAD)
                .toList();
        if (!liveAgents.isEmpty()) {
            return ResponseEntity.status(409).build();
        }

        // Get tenant ID for scoped deletion
        String tenantId = tenantProperties.getId();

        // Delete ClickHouse data
        deleteClickHouseData(tenantId, applicationId);
        routeCatalogStore.deleteByApplication(applicationId);

        // Delete managed app if exists (PostgreSQL)
        try {
            App app = appService.getBySlug(applicationId);
            appService.deleteApp(app.id());
            log.info("Dismissed managed app '{}' — deleted PG record and all CH data", applicationId);
        } catch (IllegalArgumentException e) {
            log.info("Dismissed discovered app '{}' — deleted all CH data", applicationId);
        }

        return ResponseEntity.noContent().build();
    }

    private void deleteClickHouseData(String tenantId, String applicationId) {
        String[] tablesWithAppId = {
            "executions", "processor_executions", "route_diagrams", "agent_events",
            "stats_1m_app", "stats_1m_route", "stats_1m_processor_type", "stats_1m_processor",
            "stats_1m_processor_detail", "route_catalog"
        };
        for (String table : tablesWithAppId) {
            try {
                jdbc.execute("ALTER TABLE " + table + " DELETE WHERE tenant_id = " + lit(tenantId) +
                        " AND application_id = " + lit(applicationId));
            } catch (Exception e) {
                log.warn("Failed to delete from CH table '{}' for app '{}': {}", table, applicationId, e.getMessage());
            }
        }
        // logs table uses 'application' instead of 'application_id'
        try {
            jdbc.execute("ALTER TABLE logs DELETE WHERE tenant_id = " + lit(tenantId) +
                    " AND application = " + lit(applicationId));
        } catch (Exception e) {
            log.warn("Failed to delete from CH table 'logs' for app '{}': {}", applicationId, e.getMessage());
        }
    }
}
