package com.cameleer.server.app.storage;

import com.cameleer.server.core.runtime.Deployment;
import com.cameleer.server.core.runtime.DeploymentConfigSnapshot;
import com.cameleer.server.core.runtime.DeploymentRepository;
import com.cameleer.server.core.runtime.DeploymentStatus;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.jdbc.core.JdbcTemplate;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;

public class PostgresDeploymentRepository implements DeploymentRepository {

    private static final String SELECT_COLS =
            "id, app_id, app_version_id, environment_id, status, target_state, deployment_strategy, " +
            "replica_states, deploy_stage, container_id, container_name, error_message, " +
            "resolved_config, deployed_config_snapshot, deployed_at, stopped_at, created_at, created_by";

    private final JdbcTemplate jdbc;
    private final ObjectMapper objectMapper;

    public PostgresDeploymentRepository(JdbcTemplate jdbc, ObjectMapper objectMapper) {
        this.jdbc = jdbc;
        this.objectMapper = objectMapper;
    }

    @Override
    public List<Deployment> findByAppId(UUID appId) {
        return jdbc.query(
                "SELECT " + SELECT_COLS + " FROM deployments WHERE app_id = ? ORDER BY created_at DESC",
                (rs, rowNum) -> mapRow(rs), appId);
    }

    @Override
    public List<Deployment> findByEnvironmentId(UUID environmentId) {
        return jdbc.query(
                "SELECT " + SELECT_COLS + " FROM deployments WHERE environment_id = ? ORDER BY created_at DESC",
                (rs, rowNum) -> mapRow(rs), environmentId);
    }

    @Override
    public Optional<Deployment> findById(UUID id) {
        var results = jdbc.query(
                "SELECT " + SELECT_COLS + " FROM deployments WHERE id = ?",
                (rs, rowNum) -> mapRow(rs), id);
        return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0));
    }

    @Override
    public Optional<Deployment> findActiveByAppIdAndEnvironmentId(UUID appId, UUID environmentId) {
        var results = jdbc.query(
                "SELECT " + SELECT_COLS + " FROM deployments WHERE app_id = ? AND environment_id = ? " +
                "AND status IN ('STARTING', 'RUNNING', 'DEGRADED') ORDER BY created_at DESC LIMIT 1",
                (rs, rowNum) -> mapRow(rs), appId, environmentId);
        return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0));
    }

    @Override
    public Optional<Deployment> findActiveByAppIdAndEnvironmentIdExcluding(UUID appId, UUID environmentId, UUID excludeDeploymentId) {
        var results = jdbc.query(
                "SELECT " + SELECT_COLS + " FROM deployments WHERE app_id = ? AND environment_id = ? " +
                "AND status IN ('STARTING', 'RUNNING', 'DEGRADED') AND id <> ? " +
                "ORDER BY created_at DESC LIMIT 1",
                (rs, rowNum) -> mapRow(rs), appId, environmentId, excludeDeploymentId);
        return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0));
    }

    public List<Deployment> findByStatus(List<DeploymentStatus> statuses) {
        String placeholders = String.join(",", statuses.stream().map(s -> "'" + s.name() + "'").toList());
        return jdbc.query(
                "SELECT " + SELECT_COLS + " FROM deployments WHERE status IN (" + placeholders + ") ORDER BY created_at DESC",
                (rs, rowNum) -> mapRow(rs));
    }

    @Override
    public UUID create(UUID appId, UUID appVersionId, UUID environmentId, String containerName, String createdBy) {
        UUID id = UUID.randomUUID();
        jdbc.update("INSERT INTO deployments (id, app_id, app_version_id, environment_id, container_name, created_by) VALUES (?, ?, ?, ?, ?, ?)",
                id, appId, appVersionId, environmentId, containerName, createdBy);
        return id;
    }

    @Override
    public void updateStatus(UUID id, DeploymentStatus status, String containerId, String errorMessage) {
        jdbc.update("UPDATE deployments SET status = ?, container_id = ?, error_message = ? WHERE id = ?",
                status.name(), containerId, errorMessage, id);
    }

    @Override
    public void markDeployed(UUID id) {
        jdbc.update("UPDATE deployments SET deployed_at = now() WHERE id = ?", id);
    }

    @Override
    public void markStopped(UUID id) {
        jdbc.update("UPDATE deployments SET stopped_at = now() WHERE id = ?", id);
    }

    public void updateReplicaStates(UUID id, List<Map<String, Object>> replicaStates) {
        try {
            String json = objectMapper.writeValueAsString(replicaStates);
            jdbc.update("UPDATE deployments SET replica_states = ?::jsonb WHERE id = ?", json, id);
        } catch (Exception e) {
            throw new RuntimeException("Failed to serialize replicaStates", e);
        }
    }

    public void updateDeployStage(UUID id, String stage) {
        jdbc.update("UPDATE deployments SET deploy_stage = ? WHERE id = ?", stage, id);
    }

    public void updateTargetState(UUID id, String targetState) {
        jdbc.update("UPDATE deployments SET target_state = ? WHERE id = ?", targetState, id);
    }

    public void updateDeploymentStrategy(UUID id, String strategy) {
        jdbc.update("UPDATE deployments SET deployment_strategy = ? WHERE id = ?", strategy, id);
    }

    @Override
    public void deleteFailedByAppAndEnvironment(UUID appId, UUID environmentId) {
        jdbc.update("DELETE FROM deployments WHERE app_id = ? AND environment_id = ? AND status = 'FAILED'",
                appId, environmentId);
    }

    public void updateResolvedConfig(UUID id, Map<String, Object> resolvedConfig) {
        try {
            String json = objectMapper.writeValueAsString(resolvedConfig);
            jdbc.update("UPDATE deployments SET resolved_config = ?::jsonb WHERE id = ?", json, id);
        } catch (Exception e) {
            throw new RuntimeException("Failed to serialize resolved_config", e);
        }
    }

    public void saveDeployedConfigSnapshot(UUID id, DeploymentConfigSnapshot snapshot) {
        try {
            String json = snapshot != null ? objectMapper.writeValueAsString(snapshot) : null;
            jdbc.update("UPDATE deployments SET deployed_config_snapshot = ?::jsonb WHERE id = ?", json, id);
        } catch (Exception e) {
            throw new RuntimeException("Failed to serialize deployed_config_snapshot", e);
        }
    }

    public Optional<Deployment> findLatestSuccessfulByAppAndEnv(UUID appId, UUID envId) {
        // DEGRADED deploys also carry a snapshot (executor writes before the RUNNING/DEGRADED
        // split), and represent a config that reached COMPLETE stage — restorable for the user.
        var results = jdbc.query(
                "SELECT " + SELECT_COLS + " FROM deployments "
                + "WHERE app_id = ? AND environment_id = ? "
                + "AND status IN ('RUNNING', 'DEGRADED') AND deployed_config_snapshot IS NOT NULL "
                + "ORDER BY deployed_at DESC NULLS LAST LIMIT 1",
                (rs, rowNum) -> mapRow(rs), appId, envId);
        return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0));
    }

    public Optional<Deployment> findByContainerId(String containerId) {
        var results = jdbc.query(
                "SELECT " + SELECT_COLS + " FROM deployments WHERE replica_states::text LIKE ? " +
                "AND status IN ('STARTING', 'RUNNING', 'DEGRADED') ORDER BY created_at DESC LIMIT 1",
                (rs, rowNum) -> mapRow(rs), "%" + containerId + "%");
        return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0));
    }

    private Deployment mapRow(ResultSet rs) throws SQLException {
        Timestamp deployedAt = rs.getTimestamp("deployed_at");
        Timestamp stoppedAt = rs.getTimestamp("stopped_at");
        String replicaStatesJson = rs.getString("replica_states");
        List<Map<String, Object>> replicaStates = null;
        if (replicaStatesJson != null) {
            try {
                replicaStates = objectMapper.readValue(replicaStatesJson, new TypeReference<>() {});
            } catch (Exception e) {
                throw new SQLException("Failed to deserialize replica_states", e);
            }
        }
        Map<String, Object> resolvedConfig = null;
        String resolvedConfigJson = rs.getString("resolved_config");
        if (resolvedConfigJson != null) {
            try {
                resolvedConfig = objectMapper.readValue(resolvedConfigJson, new TypeReference<>() {});
            } catch (Exception e) {
                throw new SQLException("Failed to deserialize resolved_config", e);
            }
        }
        DeploymentConfigSnapshot deployedConfigSnapshot = null;
        String snapshotJson = rs.getString("deployed_config_snapshot");
        if (snapshotJson != null) {
            try {
                deployedConfigSnapshot = objectMapper.readValue(snapshotJson, DeploymentConfigSnapshot.class);
            } catch (Exception e) {
                throw new SQLException("Failed to deserialize deployed_config_snapshot", e);
            }
        }
        return new Deployment(
                UUID.fromString(rs.getString("id")),
                UUID.fromString(rs.getString("app_id")),
                UUID.fromString(rs.getString("app_version_id")),
                UUID.fromString(rs.getString("environment_id")),
                DeploymentStatus.valueOf(rs.getString("status")),
                rs.getString("target_state"),
                rs.getString("deployment_strategy"),
                replicaStates,
                rs.getString("deploy_stage"),
                rs.getString("container_id"),
                rs.getString("container_name"),
                rs.getString("error_message"),
                resolvedConfig,
                deployedConfigSnapshot,
                deployedAt != null ? deployedAt.toInstant() : null,
                stoppedAt != null ? stoppedAt.toInstant() : null,
                rs.getTimestamp("created_at").toInstant(),
                rs.getString("created_by")
        );
    }
}
