package org.nstart.dep265.requestservice.service

import jakarta.persistence.EntityExistsException
import jakarta.persistence.EntityManager
import jakarta.persistence.PersistenceException
import org.hibernate.exception.ConstraintViolationException
import org.nstart.dep265.requestservice.dto.CreateRequestDto
import org.nstart.dep265.requestservice.dto.CreateRequestResponseDto
import org.nstart.dep265.requestservice.dto.CoverageStateDto
import org.nstart.dep265.requestservice.dto.DeleteRequestResponseDto
import org.nstart.dep265.requestservice.dto.RequestListFilter
import org.nstart.dep265.requestservice.dto.OpticsParamsDto
import org.nstart.dep265.requestservice.dto.RequestListResponseDto
import org.nstart.dep265.requestservice.dto.RequestCellResponseDto
import org.nstart.dep265.requestservice.dto.RequestResponseDto
import org.nstart.dep265.requestservice.dto.RequestSummaryResponseDto
import org.nstart.dep265.requestservice.dto.RequestWithCellsResponseDto
import org.nstart.dep265.requestservice.dto.RequestMapPageDto
import org.nstart.dep265.requestservice.dto.RequestMapItemDto
import org.nstart.dep265.requestservice.dto.RequestStatusDto
import org.nstart.dep265.requestservice.dto.RsaParamsDto
import org.nstart.dep265.requestservice.dto.SurveyTypeDto
import org.nstart.dep265.requestservice.entity.OpticsResultType
import org.nstart.dep265.requestservice.entity.RequestEntity
import org.nstart.dep265.requestservice.entity.RequestCellEntity
import org.nstart.dep265.requestservice.entity.RequestOpticsParamsEntity
import org.nstart.dep265.requestservice.entity.RequestRsaParamsEntity
import org.nstart.dep265.requestservice.entity.RequestStatus
import org.nstart.dep265.requestservice.entity.RequestSurveyType
import org.nstart.dep265.requestservice.entity.RsaPolarisation
import org.nstart.dep265.requestservice.entity.RsaResultType
import org.nstart.dep265.requestservice.repository.RequestCellRepository
import org.nstart.dep265.requestservice.repository.RequestOpticsParamsRepository
import org.nstart.dep265.requestservice.repository.RequestRepository
import org.nstart.dep265.requestservice.repository.RequestRsaParamsRepository
import org.springframework.data.domain.Page
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Sort
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.Clock
import java.time.LocalDateTime
import java.util.UUID

class InvalidRequestListSortException(message: String) : IllegalArgumentException(message)

@Service
class RequestService(
    private val requestRepository: RequestRepository,
    private val requestOpticsParamsRepository: RequestOpticsParamsRepository,
    private val requestRsaParamsRepository: RequestRsaParamsRepository,
    private val requestCellRepository: RequestCellRepository,
    private val geometryService: GeometryService,
    private val requestGridProjectionService: RequestGridProjectionService,
    private val requestLifecycleService: RequestLifecycleService,
    private val entityManager: EntityManager,
    private val clock: Clock = Clock.systemUTC(),
) {
    @Transactional
    fun createRequest(request: CreateRequestDto): CreateRequestResponseDto {
        val requestId = request.id
        if (requestRepository.existsById(requestId)) {
            throw RequestAlreadyExistsException(requestId)
        }

        require(request.beginDateTime.isBefore(request.endDateTime)) {
            "beginDateTime должен быть раньше endDateTime"
        }

        val now = UtcDateTimes.now(clock)
        val normalizedGeometry = geometryService.parsePolygonalGeometry(request.geometry)
        val normalizedGeometryWkt = geometryService.toWkt(normalizedGeometry)
        val geometryArea = geometryService.area(normalizedGeometry)
        val surveyType = request.surveyType()
        val importance = requireNotNull(request.importance) { "importance обязателен" }

        val requestEntity = RequestEntity(
            id = requestId,
            name = request.name,
            geometry = normalizedGeometryWkt,
            remainingGeometry = normalizedGeometryWkt,
            geometryArea = geometryArea,
            remainingArea = geometryArea,
            // Порог завершения по покрытию приходит из API (по умолчанию 100%); это прогресс съёмки,
            // в отличие от importance — важности заявки для карты приоритетов ячеек.
            coverageRequiredPercent = request.coverageRequiredPercent ?: 100.0,
            importance = importance,
            beginDateTime = request.beginDateTime,
            endDateTime = request.endDateTime,
            kpp = request.kpp.toMutableList(),
            highPriorityTransmit = request.highPriorityTransmit,
            surveyType = surveyType,
            status = RequestStatus.ACCEPTED,
            createdAt = now,
            updatedAt = now,
        )

        // Окно заявки может уже идти или закончиться к моменту приёма — сразу выставляем
        // корректный статус (ACCEPTED/ACTIVE/EXPIRED), а не всегда ACCEPTED.
        requestLifecycleService.refreshStatus(requestEntity, now)

        persistNewRequest(requestEntity)
        replaceSurveyParams(requestEntity.id, request.optics, request.rsa)
        requestGridProjectionService.rebuildProjection(requestEntity)

        return CreateRequestResponseDto(
            id = requestEntity.id,
            name = requestEntity.name,
            status = requestEntity.status.toDto(),
            surveyType = requestEntity.surveyType.toDto(),
            importance = requestEntity.importance,
            message = "Заявка успешно создана",
        )
    }

    @Transactional(readOnly = true)
    fun listRequests(query: RequestListFilter): RequestListResponseDto {
        val requests = findRequests(query)
        return RequestListResponseDto(
            items = requests.content.map { request -> request.toSummaryResponse() },
            page = query.page,
            size = query.size,
            totalItems = requests.totalElements,
            totalPages = requests.totalPages,
        )
    }

    @Transactional(readOnly = true)
    fun listRequestsForMap(query: RequestListFilter): RequestMapPageDto {
        val requests = findRequests(query)
        return RequestMapPageDto(
            items = requests.content.map { request -> request.toMapItemResponse() },
            page = query.page,
            size = query.size,
            totalItems = requests.totalElements,
            totalPages = requests.totalPages,
        )
    }

    @Transactional(readOnly = true)
    fun getRequestById(id: UUID): RequestResponseDto? {
        val request = requestRepository.findById(id).orElse(null) ?: return null
        if (request.status == RequestStatus.DELETED) {
            return null
        }
        return request.toResponse()
    }

    @Transactional(readOnly = true)
    fun getRequestWithCells(id: UUID): RequestWithCellsResponseDto? {
        val request = requestRepository.findById(id).orElse(null) ?: return null
        val cells = requestCellRepository.findWithCellByRequestId(id)
            .map { requestCell -> requestCell.toRequestCellResponse() }

        return RequestWithCellsResponseDto(
            request = request.toResponse(),
            cells = cells,
        )
    }

    @Transactional
    fun deleteRequest(id: UUID): DeleteRequestResponseDto? {
        val request = requestRepository.findById(id).orElse(null) ?: return null
        val deletedAt = request.deletedAt ?: UtcDateTimes.now(clock)
        request.status = RequestStatus.DELETED
        request.deletedAt = deletedAt
        request.updatedAt = deletedAt
        requestRepository.save(request)
        requestCellRepository.deleteByRequestId(request.id)
        return DeleteRequestResponseDto(
            id = request.id,
            deletedAt = deletedAt,
        )
    }

    private fun persistNewRequest(requestEntity: RequestEntity) {
        try {
            entityManager.persist(requestEntity)
            entityManager.flush()
        } catch (exception: EntityExistsException) {
            throw RequestAlreadyExistsException(requestEntity.id, exception)
        } catch (exception: PersistenceException) {
            if (exception.isDuplicateRequestId()) {
                throw RequestAlreadyExistsException(requestEntity.id, exception)
            }
            throw exception
        }
    }

    private fun replaceSurveyParams(requestId: UUID, optics: OpticsParamsDto?, rsa: RsaParamsDto?) {
        if (optics == null) {
            if (requestOpticsParamsRepository.existsById(requestId)) {
                requestOpticsParamsRepository.deleteById(requestId)
            }
        } else {
            requestOpticsParamsRepository.save(optics.toEntity(requestId))
        }

        if (rsa == null) {
            if (requestRsaParamsRepository.existsById(requestId)) {
                requestRsaParamsRepository.deleteById(requestId)
            }
        } else {
            requestRsaParamsRepository.save(rsa.toEntity(requestId))
        }
    }

    private fun findRequests(query: RequestListFilter): Page<RequestEntity> =
        requestRepository.findRequests(
            includeDeleted = query.includeDeleted,
            deletedStatus = RequestStatus.DELETED,
            status = query.status?.toEntity(),
            surveyType = query.surveyType?.toEntity(),
            kpp = query.kpp,
            highPriorityTransmit = query.highPriorityTransmit,
            beginFrom = query.beginFrom,
            beginTo = query.beginTo,
            endFrom = query.endFrom,
            endTo = query.endTo,
            pageable = PageRequest.of(query.page, query.size, query.sort.toRequestSort()),
        )

    private fun RequestEntity.toResponse(): RequestResponseDto {
        val optics = requestOpticsParamsRepository.findById(id).orElse(null)?.toDto()
        val rsa = requestRsaParamsRepository.findById(id).orElse(null)?.toDto()
        return RequestResponseDto(
            id = id,
            name = name,
            status = status.toDto(),
            surveyType = surveyType.toDto(),
            geometry = geometry,
            importance = importance,
            beginDateTime = beginDateTime,
            endDateTime = endDateTime,
            kpp = kpp,
            highPriorityTransmit = highPriorityTransmit,
            optics = optics,
            rsa = rsa,
            coverage = coverage(),
            createdAt = createdAt,
            updatedAt = updatedAt,
            deletedAt = deletedAt,
        )
    }

    private fun RequestCellEntity.toRequestCellResponse(): RequestCellResponseDto {
        return RequestCellResponseDto(
            cellNum = requireNotNull(cell?.cellNum) { "request_cell must be linked to earth_cell" },
            coveragePercent = coveragePercent ?: 0.0,
            importance = importance,
            contour = contour,
        )
    }

    private fun RequestEntity.toSummaryResponse(): RequestSummaryResponseDto {
        return RequestSummaryResponseDto(
            id = id,
            name = name,
            status = status.toDto(),
            surveyType = surveyType.toDto(),
            importance = importance,
            beginDateTime = beginDateTime,
            endDateTime = endDateTime,
            kpp = kpp,
            highPriorityTransmit = highPriorityTransmit,
            coveragePercent = coverage().currentPercent,
            createdAt = createdAt,
            updatedAt = updatedAt,
            deletedAt = deletedAt,
        )
    }

    private fun RequestEntity.toMapItemResponse(): RequestMapItemDto {
        return RequestMapItemDto(
            id = id,
            name = name,
            status = status.toDto(),
            surveyType = surveyType.toDto(),
            geometry = geometry,
            importance = importance,
            beginDateTime = beginDateTime,
            endDateTime = endDateTime,
            kpp = kpp,
            highPriorityTransmit = highPriorityTransmit,
            coveragePercent = coverage().currentPercent,
            createdAt = createdAt,
            updatedAt = updatedAt,
            deletedAt = deletedAt,
        )
    }

    private fun RequestEntity.coverage(): CoverageStateDto {
        return CoverageStateDto(
            requiredPercent = coverageRequiredPercent,
            currentPercent = geometryService.coveredPercent(geometryArea, remainingArea),
        )
    }

    private fun CreateRequestDto.surveyType(): RequestSurveyType {
        return when {
            optics != null && rsa != null -> RequestSurveyType.COMBINED
            optics != null -> RequestSurveyType.OPTICS
            rsa != null -> RequestSurveyType.RSA
            else -> throw IllegalArgumentException("Должен быть указан хотя бы один блок съемки: optics или rsa")
        }
    }

    private fun OpticsParamsDto.toEntity(requestId: UUID): RequestOpticsParamsEntity {
        return RequestOpticsParamsEntity(
            requestId = requestId,
            resultType = OpticsResultType.valueOf(resultType.name),
            resolution = resolution,
            sunAngleMin = sunAngleMin,
            sunAngleMax = sunAngleMax,
            clouds = clouds,
        )
    }

    private fun RequestOpticsParamsEntity.toDto(): OpticsParamsDto {
        return OpticsParamsDto(
            resultType = org.nstart.dep265.requestservice.dto.OpticsResultTypeDto.valueOf(resultType.name),
            resolution = resolution,
            sunAngleMin = sunAngleMin,
            sunAngleMax = sunAngleMax,
            clouds = clouds,
        )
    }

    private fun RsaParamsDto.toEntity(requestId: UUID): RequestRsaParamsEntity {
        return RequestRsaParamsEntity(
            requestId = requestId,
            resultType = RsaResultType.valueOf(resultType.name),
            interferometry = interferometry,
            polarisation = RsaPolarisation.valueOf(polarisation.name),
            resolution = resolution,
        )
    }

    private fun RequestRsaParamsEntity.toDto(): RsaParamsDto {
        return RsaParamsDto(
            resultType = org.nstart.dep265.requestservice.dto.RsaResultTypeDto.valueOf(resultType.name),
            interferometry = interferometry,
            polarisation = org.nstart.dep265.requestservice.dto.PolarisationDto.valueOf(polarisation.name),
            resolution = resolution,
        )
    }

    private fun RequestStatus.toDto(): RequestStatusDto = RequestStatusDto.valueOf(name)

    private fun RequestSurveyType.toDto(): SurveyTypeDto = SurveyTypeDto.valueOf(name)

    private fun RequestStatusDto.toEntity(): RequestStatus = RequestStatus.valueOf(name)

    private fun SurveyTypeDto.toEntity(): RequestSurveyType = RequestSurveyType.valueOf(name)

    private fun String.toRequestSort(): Sort {
        val parts = split(",").map { part -> part.trim() }.filter { part -> part.isNotEmpty() }
        val field = parts.firstOrNull() ?: DEFAULT_SORT_FIELD
        val direction = parts.getOrNull(1)?.let { value ->
            when (value.lowercase()) {
                "asc" -> Sort.Direction.ASC
                "desc" -> Sort.Direction.DESC
                else -> throw InvalidRequestListSortException("Unsupported sort direction: $value")
            }
        } ?: DEFAULT_SORT_DIRECTION
        val property = ALLOWED_SORT_FIELDS[field]
            ?: throw InvalidRequestListSortException("Unsupported sort field: $field")
        return Sort.by(direction, property)
    }

    private fun Throwable.isDuplicateRequestId(): Boolean {
        return generateSequence(this) { throwable -> throwable.cause }
            .filterIsInstance<ConstraintViolationException>()
            .any { exception ->
                exception.sqlState == POSTGRES_UNIQUE_VIOLATION &&
                    (exception.constraintName == null || exception.constraintName == REQUESTS_PRIMARY_KEY)
            }
    }

    private companion object {
        const val POSTGRES_UNIQUE_VIOLATION = "23505"
        const val REQUESTS_PRIMARY_KEY = "requests_pkey"
        const val DEFAULT_SORT_FIELD = "createdAt"
        val DEFAULT_SORT_DIRECTION: Sort.Direction = Sort.Direction.DESC
        val ALLOWED_SORT_FIELDS: Map<String, String> = mapOf(
            "createdAt" to "createdAt",
            "updatedAt" to "updatedAt",
            "beginDateTime" to "beginDateTime",
            "endDateTime" to "endDateTime",
            "status" to "status",
            "surveyType" to "surveyType",
            "importance" to "importance",
        )
    }
}
