package space.nstart.pcp.pcp_stations_service.service


import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import space.nstart.pcp.pcp_stations_service.configuration.ClassifierProperties
import space.nstart.pcp.pcp_stations_service.configuration.CustomErrorException
import space.nstart.pcp.pcp_stations_service.entity.StationEntity
import space.nstart.pcp.pcp_stations_service.integration.NsiStationClient
import space.nstart.pcp.pcp_stations_service.integration.NsiStationsUnavailableException
import space.nstart.pcp.pcp_stations_service.repository.StationRepository
import space.nstart.pcp.pcp_types_lib.dto.ballistics.StationDTO
import java.util.UUID
import java.time.Clock
import java.time.Instant

@Service
class StationService(
    private val stationRepository: StationRepository,
    private val nsiStationClient: NsiStationClient,
    private val classifierProperties: ClassifierProperties,
    private val clock: Clock = Clock.systemUTC()
) {

    private val log = LoggerFactory.getLogger(javaClass)

    /*
     * В каждый момент времени реальный refresh выполняет только один поток.
     * Остальные запросы ожидают результат этого refresh.
     */
    private val cacheLock = Any()

    @Volatile
    private var cachedStations: StationsCache? = null

    /*
     * После полного провала refresh временно не создаем нового лидера,
     * чтобы очередь запросов не начала последовательно долбить лежащую НСИ.
     */
    @Volatile
    private var refreshBlockedUntil: Instant = Instant.EPOCH


    fun all(): List<StationDTO> =
        loadStationsCache().stations

    fun byId(id: UUID): StationDTO =
        loadStationsCache().byId[id]
            ?: throw CustomErrorException("Станция $id не найдена")


    fun add(station : StationDTO): StationDTO = stationRepository.save(StationEntity(station)).toDTO()

    fun delete(id : UUID): Int = stationRepository.deleteByStationId(id)

    private fun loadStationsCache(): StationsCache {
        val now = Instant.now(clock)

        /*
         * быстрый путь без блокировки
         */
        cachedStations
            ?.takeIf { cache -> cache.isFresh(now) }
            ?.let { cache ->
                log.debug(
                    "Stations cache hit. cachedCount={}, expiresAt={}",
                    cache.stations.size,
                    cache.expiresAt
                )
                return cache
            }

        /*
         * прошлый рефреш полностью провалился.
         * во время колдауна НСИ повторно не вызываем.
         */
        if (now.isBefore(refreshBlockedUntil)) {
            return cachedStations ?: throwDataUnavailable()
        }

        return synchronized(cacheLock) {
            val lockedNow = Instant.now(clock)

            /*
             * двойная проверка пока запрос ожидал лок, другой поток мог
             * уже успешно обновить кэш.
             */
            cachedStations
                ?.takeIf { cache -> cache.isFresh(lockedNow) }
                ?: if (lockedNow.isBefore(refreshBlockedUntil)) {
                    cachedStations ?: throwDataUnavailable()
                } else {
                    refreshStationsOrFallback()
                }
        }
    }

    private fun refreshStationsOrFallback(): StationsCache {
        return try {
            val stations = fetchStationsWithRetry()
            val now = Instant.now(clock)

            val snapshotStations = stations.toList()

            val refreshedCache = StationsCache(
                stations = snapshotStations,
                byId = snapshotStations
                    .mapNotNull { station ->
                        station.id?.let { id -> id to station }
                    }
                    .toMap(),
                expiresAt = now.plus(classifierProperties.stationsCacheTtl)
            )

            cachedStations = refreshedCache
            refreshBlockedUntil = Instant.EPOCH

            log.info(
                "Stations cache refreshed. cachedCount={}, expiresAt={}",
                refreshedCache.stations.size,
                refreshedCache.expiresAt
            )

            refreshedCache
        } catch (ex: NsiStationsUnavailableException) {
            refreshBlockedUntil = Instant.now(clock)
                .plus(classifierProperties.stationsRefreshFailureCooldown)

            val staleCache = cachedStations

            if (staleCache != null) {
                log.warn(
                    "NSI stations are unavailable after refresh attempts. " +
                            "Returning stale cache. cachedCount={}, cacheExpiredAt={}, " +
                            "nextRefreshAllowedAt={}, status={}",
                    staleCache.stations.size,
                    staleCache.expiresAt,
                    refreshBlockedUntil,
                    ex.statusCode,
                    ex
                )

                staleCache
            } else {
                log.warn(
                    "NSI stations are unavailable and local cache is empty. " +
                            "nextRefreshAllowedAt={}, status={}",
                    refreshBlockedUntil,
                    ex.statusCode,
                    ex
                )

                throwDataUnavailable(ex)
            }
        }
    }

    private fun fetchStationsWithRetry(): List<StationDTO> {
        val attempts = classifierProperties.stationsRefreshAttempts
            .coerceAtLeast(1)

        var lastFailure: NsiStationsUnavailableException? = null

        for (attempt in 1..attempts) {
            try {
                log.info(
                    "Refreshing stations from NSI. attempt={}/{}",
                    attempt,
                    attempts
                )

                return nsiStationClient.fetchStations()
            } catch (ex: NsiStationsUnavailableException) {
                lastFailure = ex

                if (attempt == attempts) {
                    break
                }

                log.warn(
                    "NSI stations refresh attempt failed. " +
                            "attempt={}/{}, status={}, retryDelay={}",
                    attempt,
                    attempts,
                    ex.statusCode,
                    classifierProperties.stationsRefreshRetryDelay
                )

                try {
                    Thread.sleep(
                        classifierProperties.stationsRefreshRetryDelay
                            .toMillis()
                            .coerceAtLeast(0)
                    )
                } catch (interrupted: InterruptedException) {
                    Thread.currentThread().interrupt()
                    throw ex
                }
            }
        }

        throw checkNotNull(lastFailure)
    }

    private fun throwDataUnavailable(
        cause: Throwable? = null
    ): Nothing {
        val retryAfterSeconds = classifierProperties
            .stationsRefreshFailureCooldown
            .seconds
            .coerceAtLeast(1)

        throw StationsDataUnavailableException(
            message = "Станции временно недоступны: НСИ не отвечает, локальный кэш пуст",
            retryAfterSeconds = retryAfterSeconds,
            cause = cause
        )
    }

    private data class StationsCache(
        val stations: List<StationDTO>,
        val byId: Map<UUID, StationDTO>,
        val expiresAt: Instant
    ) {
        fun isFresh(now: Instant): Boolean =
            now.isBefore(expiresAt)
    }

}

