package ru.yandex.direct.web.entity.uac.service

import java.net.MalformedURLException
import java.net.URL
import org.slf4j.LoggerFactory
import org.springframework.context.i18n.LocaleContextHolder
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Service
import org.springframework.web.util.UriComponentsBuilder
import ru.yandex.altay.model.language.LanguageOuterClass
import ru.yandex.direct.common.util.HttpUtil
import ru.yandex.direct.core.entity.adgeneration.ImageGenerationService
import ru.yandex.direct.core.entity.adgeneration.model.ImageSuggest
import ru.yandex.direct.core.entity.campaign.model.MetrikaCounterSource
import ru.yandex.direct.core.entity.campaign.service.CampMetrikaCountersService
import ru.yandex.direct.core.entity.campaign.service.CampMetrikaCountersService.CounterWithAdditionalInformationFilter
import ru.yandex.direct.core.entity.organizations.service.OrganizationService
import ru.yandex.direct.core.entity.uac.model.AppInfo
import ru.yandex.direct.core.entity.uac.model.Content
import ru.yandex.direct.core.entity.uac.model.CreativeType
import ru.yandex.direct.core.entity.uac.model.MediaType
import ru.yandex.direct.core.entity.uac.repository.ydb.UacYdbAppInfoRepository
import ru.yandex.direct.core.entity.uac.repository.ydb.UacYdbUtils.YDB_MAX_ROWS_COUNT
import ru.yandex.direct.core.entity.uac.repository.ydb.model.UacYdbAppInfo
import ru.yandex.direct.core.entity.uac.repository.ydb.model.UacYdbCampaign
import ru.yandex.direct.core.entity.uac.repository.ydb.model.UacYdbContent
import ru.yandex.direct.core.entity.uac.service.UacAppInfoService
import ru.yandex.direct.core.entity.uac.service.UacCampaignServiceHolder
import ru.yandex.direct.core.entity.user.model.User
import ru.yandex.direct.organizations.swagger.OrganizationsClient
import ru.yandex.direct.result.Result
import ru.yandex.direct.validation.result.Defect
import ru.yandex.direct.validation.result.DefectInfo
import ru.yandex.direct.web.core.entity.mobilecontent.service.WebCoreMobileAppService
import ru.yandex.direct.web.entity.uac.converter.UacLibraryAppConverter
import ru.yandex.direct.web.entity.uac.converter.UacSaasResponseConverter
import ru.yandex.direct.web.entity.uac.converter.UacSiteSuggestConverter
import ru.yandex.direct.web.entity.uac.converter.UacUniqAppByBundleIdConverter
import ru.yandex.direct.web.entity.uac.model.CreateContentRequest
import ru.yandex.direct.web.entity.uac.model.SaasRequest
import ru.yandex.direct.web.entity.uac.model.Suggest
import ru.yandex.direct.web.entity.uac.model.UacSiteSuggest
import ru.yandex.direct.web.entity.uac.model.UacUniqAppByBundleId
import ru.yandex.direct.web.entity.uac.toResponse
import ru.yandex.direct.web.entity.uac.validation.UacWebSuggestDefect
import kotlin.collections.HashMap

@Service
class UacSuggestService(
    private val uacYdbAppInfoRepository: UacYdbAppInfoRepository,
    private val uacAppMetrikaService: UacAppMetrikaService,
    private val uacAppInfoService: UacAppInfoService,
    private val webCoreMobileAppService: WebCoreMobileAppService,
    private val uacSaasClient: UacSaasClient,
    private val campMetrikaCountersService: CampMetrikaCountersService,
    private val organizationService: OrganizationService,
    private val imageGenerationService: ImageGenerationService,
    private val uacCampaignServiceHolder: UacCampaignServiceHolder,
    private val uacCampaignWebServiceHolder: UacCampaignWebServiceHolder,
) {
    companion object {
        private val logger = LoggerFactory.getLogger(UacSuggestService::class.java)
        private const val WARNING_KEY = "warnings"
        private const val ERROR_KEY = "errors"
    }

    fun suggest(user: User, text: String?, limit: Int): Suggest {
        val library = suggestLibraryApps(user, text)
        val appMetrika = suggestAppMetrikaApps(user, text)
        return Suggest(
            library = library.take(limit),
            saas = suggestSaasApps(text, limit).take(limit),
            appMetrika = filterAppMetrikaApps(appMetrika, library).take(limit),
            webUrls = suggestWebUrls(user, text).take(limit),
        )
    }

    fun suggestAppMetrikaApps(user: User, text: String?): List<AppInfo> {
        val appMetrikaApps = uacAppMetrikaService
            .getApps(user)
            .map { UacUniqAppByBundleIdConverter.fromUacAppMetrikaApp(it) }
        val appInfos = getAppInfo(appMetrikaApps)

        return appInfos
            .filter { corresponds(it.title, text) }
    }

    fun suggestLibraryApps(user: User, text: String?): List<AppInfo> {
        val libraryApps = webCoreMobileAppService
            .getAppList(user.clientId, null, null)
            .map { UacLibraryAppConverter.toUniqAppByBundleId(it) }
        val appInfos = getAppInfo(libraryApps)
        return appInfos
            .filter { corresponds(it.title, text) }
    }

    fun suggestSaasApps(text: String?, limit: Int): List<AppInfo> {
        // делаем отдельные запросы по каждому стору
        val suggests = uacSaasClient.suggest(listOf(
            SaasRequest(text, "google_play"),
            SaasRequest(text, "itunes")
        ), limit)
        var firstIds = UacSaasResponseConverter.responseToDatabaseIds(suggests[0])
        var secondIds = UacSaasResponseConverter.responseToDatabaseIds(suggests[1])

        // объединяем саджесты по платформам в перекрестном порядке
        // приоритетную платформу выбираем по максимальной релевантности
        if (UacSaasResponseConverter.responseToMaxRelevance(suggests[0]) <
            UacSaasResponseConverter.responseToMaxRelevance(suggests[1])) {
            firstIds = secondIds.also { secondIds = firstIds }
        }
        val databaseIds = sequence {
            val first = firstIds.iterator()
            val second = secondIds.iterator()
            while (first.hasNext() && second.hasNext()) {
                yield(first.next())
                yield(second.next())
            }

            yieldAll(first)
            yieldAll(second)
        }.toList().take(limit)

        val order = databaseIds.distinct().withIndex().associate { it.value to it.index }
        val appInfoYdb = uacYdbAppInfoRepository.getAppInfoByIds(databaseIds).sortedBy { order[it.id] }
        return appInfoYdb
            .map { uacAppInfoService.getAppInfo(it, addRecommendations = false) }
    }

    fun suggestWebUrls(user: User, text: String?): List<UacSiteSuggest> {
        val counterFilter = CounterWithAdditionalInformationFilter
            .defaultFilter()
            .withAllowedSources(mutableSetOf(MetrikaCounterSource.UNKNOWN))
        val counters = campMetrikaCountersService.getAvailableCountersByClientId(
            user.clientId,
            counterFilter)
        val metrikaSites = counters
            .map { UacSiteSuggestConverter.metrikaCounterToSiteSuggest(it) }
        val language = OrganizationsClient.getLanguageByName(
            LocaleContextHolder.getLocale().language).orElse(LanguageOuterClass.Language.EN)
        val organizationsData = organizationService.getApiClientOrganizations(
            user.clientId,
            user.chiefUid,
            language,
            null,
        )
        val organizationsSites = organizationsData
            .mapNotNull { UacSiteSuggestConverter.organizationToSiteSuggest(it) }
        return metrikaSites
            .union(organizationsSites)
            .filter { corresponds(it.title, text) }
    }

    private fun filterAppMetrikaApps(appMetrika: List<AppInfo>, library: List<AppInfo>): List<AppInfo> {
        val libraryBundleIds = library
            .map { Pair(it.bundleId, it.platform) }
            .toSet()
        return appMetrika.filter { !libraryBundleIds.contains(Pair(it.bundleId, it.platform)) }
    }

    private fun corresponds(objName: String?, text: String?): Boolean {
        if (text == null) {
            return true
        }
        return objName != null && objName.lowercase().contains(text.lowercase())
    }

    private fun getAppInfo(apps: List<UacUniqAppByBundleId>): List<AppInfo> {
        if (apps.size > YDB_MAX_ROWS_COUNT) {
            logger.error("Too many apps ${apps.size}")
        }

        val ydbAppInfoByAppMetricaApp = mutableMapOf<UacUniqAppByBundleId, UacYdbAppInfo>()

        uacYdbAppInfoRepository
            .getAppInfoByBundleIds(apps.map { it.bundleId }.toSet())
            .forEach {
                ydbAppInfoByAppMetricaApp[UacUniqAppByBundleId(it.bundleId!!, it.platform, it.source)] = it
            }

        return apps
            .mapNotNull { ydbAppInfoByAppMetricaApp[it] }
            .distinctBy { listOf(it.bundleId, it.source, it.region, it.language) }
            .map { uacAppInfoService.getAppInfo(it) }
    }

    /**
     * Возвращаем ответ с саджестами картинок в виде сохраненных ассетов из БД с id.
     * Если у переданной кампании уже есть картинка, полученная ранее от саджеста - берем ее данные из БД
     * Если у переданной кампании нет картинки из саджеста - сохраняем в БД и отправляем с новым id
     */
    fun suggestImages(
        subjectUser: User,
        operator: User,
        uacCampaign: UacYdbCampaign,
        useGrut: Boolean,
        directCampaignId: Long,
        adGroupId: Long?,
        text: String?,
        url: String?,
    ): ResponseEntity<Any> {

        val campaignUrl = url ?: uacCampaign.storeUrl

        // Получаем саджесты картинок
        val additionalInfo: MutableMap<String, Any> = HashMap()
        val suggests: Result<Collection<ImageSuggest>> =
            imageGenerationService.generateImages(
                subjectUser.clientId,
                campaignUrl,
                text,
                null,
                adGroupId,
                additionalInfo
            )

        val errors = mutableListOf<UacWebSuggestDefect>()
        val warning = mutableListOf<UacWebSuggestDefect>()
        warning.addAll(getWebDefectList(suggests.warnings))
        additionalInfo[WARNING_KEY] = warning

        if (!suggests.isSuccessful) {
            logger.error("An error occurred while receiving suggests")
            return ResponseEntity(listOf<Content>().toResponse(additionalInfo), HttpStatus.BAD_REQUEST)
        }
        logger.info("Got ${suggests.result.size} image suggestions")

        val uacContentService = uacCampaignServiceHolder.getUacContentService(useGrut)
        val uacCampaignWebService = uacCampaignWebServiceHolder.getUacContentWebService(useGrut)

        // Собираем текущие ассеты кампании
        val imageContents = uacContentService.getContentsByDirectCampaignId(
            directCampaignId, subjectUser.clientId, MediaType.IMAGE
        )
        val contentByImageHash = imageContents
            .filter { !it.directImageHash.isNullOrBlank() }
            .associateBy { it.directImageHash }
        val contentBySourceUrl = imageContents
            .associateBy { it.sourceUrl }
        logger.info("Got ${imageContents.size} images for $directCampaignId campaign")

        // Сохраняем саджесты картинок, собираем добавленные, заменяя те что уже есть в кампании
        val suggestedContents = mutableListOf<Content>()
        val contentsToAdd = mutableListOf<UacYdbContent>()
        suggests.result.forEach {

            // Меняем формат ссылки для корректной загрузки картинки DIRECT-138406
            val imageUrl = getImageUrl(it.url)

            val contentRequest = CreateContentRequest(
                type = MediaType.IMAGE,
                thumb = null,
                sourceUrl = imageUrl,
                mdsUrl = null,
            )

            val contentResult = uacCampaignWebService
                .uploadContent(
                    operator,
                    subjectUser,
                    CreativeType.TGO,
                    null,
                    contentRequest,
                    HttpUtil.getCurrentLocale().orElse(null)
                )

            if (contentResult.isSuccessful) {
                val content = contentResult.result

                // Считаем картинку существующей в кампании по совпадению imageHash или sourceUrl
                if (contentByImageHash.containsKey(content.directImageHash)) {
                    suggestedContents.add(contentByImageHash[content.directImageHash]!!)
                } else if (contentBySourceUrl.containsKey(content.sourceUrl)) {
                    suggestedContents.add(contentBySourceUrl[content.sourceUrl]!!)
                } else {
                    suggestedContents.add(uacContentService.fillContent(content)!!)
                    contentsToAdd.add(content)
                }
            } else {
                warning.addAll(getWebDefectList(contentResult.validationResult.flattenWarnings()))
                errors.addAll(getWebDefectList(contentResult.validationResult.flattenErrors()))
            }
        }

        // Добавляем новый контент в БД
        if (contentsToAdd.isNotEmpty()) {
            uacContentService.insertContents(contentsToAdd)
        }

        additionalInfo[WARNING_KEY] = warning
        additionalInfo[ERROR_KEY] = errors
        if (errors.isNotEmpty()) {
            logger.error("Image save operation has ${errors.size} errors. First error: ${errors.first()}")
        }
        logger.info("Sending ${suggestedContents.size} images by suggestions, with " +
            "${suggestedContents.size - contentsToAdd.size} images that are already in the campaign $directCampaignId")

        return ResponseEntity(suggestedContents.toResponse(additionalInfo), HttpStatus.OK)
    }

    private fun getImageUrl(imageUrl: String?): String? {
        val parsed = try {
            URL(imageUrl)
        } catch (e: MalformedURLException) {
            null
        }
        if (parsed != null) {
            return UriComponentsBuilder.fromUri(parsed.toURI())
                .scheme("https")
                .replaceQueryParam("n", 13)
                .build()
                .toString()
        }
        return imageUrl
    }

    private fun getWebDefectList(defectInfos: List<DefectInfo<Defect<*>>>): Collection<UacWebSuggestDefect> {
        return defectInfos
            .map {
                UacWebSuggestDefect(
                    defectId = it.defect.defectId(),
                    value = if (it.value != null) it.value.toString() else null
                )
            }
    }
}
