import asyncio
import logging
import re
import time
from datetime import timedelta
from typing import Any, Dict, List, Optional, Tuple, Union

from frozendict import frozendict
from yandex.maps.proto.search import hours_pb2

from maps_adv.common.avatars import AvatarsClient
from maps_adv.common.ugcdb_client import UgcdbClient
from maps_adv.config_loader import Config
from maps_adv.geosmb.clients.bunker import BunkerClient
from maps_adv.geosmb.clients.bvm import BvmClient
from maps_adv.geosmb.clients.geobase import GeoBaseClient
from maps_adv.geosmb.clients.geosearch import (
    AddressComponent,
    GeoSearchClient,
    GeoSearchOrgResult,
)
from maps_adv.geosmb.clients.market import MarketIntClient
from maps_adv.geosmb.landlord.server.lib.data_manager import BaseDataManager
from maps_adv.geosmb.landlord.server.lib.enums import Feature, LandingVersion
from maps_adv.geosmb.landlord.server.lib.exceptions import (
    InvalidFetchToken,
    NoBizIdForOrg,
    NoDataForBizId,
    NoDataForSlug,
    NoOrginfo,
    NoOrgsForBizId,
    NoStableVersionForPublishing,
    UnknownColorPreset,
)
from maps_adv.geosmb.landlord.server.lib.ext_feed_lb_writer import (
    ExtFeedLogbrokerWriter,
)
from maps_adv.geosmb.tuner.client import TunerClient

from ..async_yt_client import AsyncYtClient
from ..timing import TimeDiff, async_time_diff
from .rating_provider import RatingProvider
from .service_provider import ServiceProvider
from .slug_maker import SlugMaker
from .suggester import Suggester

_tpl_postfix_re = re.compile("%s$")


async def async_noop():
    return None


class Domain:
    color_presets = {
        "YELLOW": dict(
            main_color_hex="FFD353",
            text_color_over_main="DARK",
            main_color_name="YELLOW",
        ),
        "GREEN": dict(
            main_color_hex="00E087",
            text_color_over_main="DARK",
            main_color_name="GREEN",
        ),
        "VIOLET": dict(
            main_color_hex="6951FF",
            text_color_over_main="LIGHT",
            main_color_name="VIOLET",
        ),
        "RED": dict(
            main_color_hex="FB524F", text_color_over_main="LIGHT", main_color_name="RED"
        ),
        "BLUE": dict(
            main_color_hex="3083FF",
            text_color_over_main="LIGHT",
            main_color_name="BLUE",
        ),
    }

    default_preset = "YELLOW"
    default_color_theme = frozendict({"theme": "LIGHT", "preset": default_preset})

    _dm: BaseDataManager
    _config: Config
    _bvm_client: BvmClient
    _geosearch_client: GeoSearchClient
    _geobase_client: GeoBaseClient
    _market_client: MarketIntClient
    _avatars_client: AvatarsClient
    _avatars_upload_timeout: int
    _ugcdb_client: UgcdbClient
    _yt_client: AsyncYtClient
    _bunker_client: BunkerClient
    _tuner_client: TunerClient
    _fetch_data_token: str

    _service_provider: ServiceProvider
    _suggester: Suggester
    _slug_maker: SlugMaker

    _landing_config_bunker_node: str
    _landing_config_bunker_node_version: str

    def __init__(
        self,
        dm: BaseDataManager,
        config: Config,
        bvm_client: BvmClient,
        geosearch_client: GeoSearchClient,
        geobase_client: GeoBaseClient,
        market_client: MarketIntClient,
        avatars_client: AvatarsClient,
        avatars_upload_timeout: int,
        ugcdb_client: UgcdbClient,
        yt_client: AsyncYtClient,
        bunker_client: BunkerClient,
        tuner_client: TunerClient,
        ext_feed_writer: ExtFeedLogbrokerWriter,
        fetch_data_token: str,
        base_maps_url: str,
        base_widget_request_url: str,
        landing_config_bunker_node: str,
        landing_config_bunker_node_version: str,
        disable_promoted_services: bool = False,
    ):
        self._dm = dm
        self._config = config
        self._bvm_client = bvm_client
        self._geosearch_client = geosearch_client
        self._market_client = market_client
        self._avatars_client = avatars_client
        self._avatars_upload_timeout = avatars_upload_timeout
        self._ugcdb_client = ugcdb_client
        self._fetch_data_token = fetch_data_token
        self._geobase_client = geobase_client
        self._yt_client = yt_client
        self._bunker_client = bunker_client
        self._tuner_client = tuner_client
        self._ext_feed_writer = ext_feed_writer

        self._service_provider = ServiceProvider(
            dm=dm,
            market_client=market_client,
            disable_promoted_services=disable_promoted_services,
        )
        self._suggester = Suggester(
            dm, geosearch_client, base_maps_url, base_widget_request_url
        )
        self._slug_maker = SlugMaker(self.check_slug_is_free)

        self._landing_config_bunker_node = landing_config_bunker_node
        self._landing_config_bunker_node_version = landing_config_bunker_node_version

    async def generate_data_for_biz_id(
        self,
        biz_id: Optional[int] = None,
        permalink: Optional[int] = None,
        publish: bool = False,
    ) -> str:

        logging.getLogger(__name__).info(
            f"[GEN_LAND] Generate landing biz_id {biz_id}, permalink {permalink}"
        )

        if (
            biz_id is None
            and permalink is None
            or biz_id is not None
            and permalink is not None
        ):
            raise ValueError

        if permalink is not None:
            biz_id = await self._bvm_client.fetch_biz_id_by_permalink(
                permalink=permalink
            )
            logging.getLogger(__name__).info(
                f"[GEN_LAND] Found biz_id {biz_id} for permalink {permalink}"
            )

        biz_state = await self._dm.fetch_biz_state(biz_id=biz_id)
        logging.getLogger(__name__).info(
            f"[GEN_LAND] Current biz state {biz_state} for biz_id {biz_id}"
        )

        if (
            biz_state is not None
            and biz_state["stable_version"] is not None
            and biz_state["unstable_version"] is not None
        ):
            return biz_state["slug"]

        if permalink is None:
            permalinks = await self._bvm_client.fetch_permalinks_by_biz_id(
                biz_id=biz_id
            )

            logging.getLogger(__name__).info(
                f"[GEN_LAND] Found permalinks {permalinks} for biz_id {biz_id}"
            )

            if not permalinks:
                raise NoOrgsForBizId
            permalink = permalinks[0]

        orginfo = await self._geosearch_client.resolve_org(permalink=permalink)

        logging.getLogger(__name__).info(
            f"[GEN_LAND] Org info for permalink {permalink} - {orginfo}"
        )

        if orginfo is None:
            raise NoOrginfo

        landing_data = {
            "name": orginfo.name,
            "categories": orginfo.categories_names,
            "contacts": await self._geosearch_extract_contacts(orginfo),
            "extras": {"plain_extras": []},
            "preferences": {
                "color_theme": dict(self.default_color_theme),
            },
        }
        await self._enrich_geo_data(landing_data=landing_data, orginfo=orginfo)

        await self._compose_landing_data_from_orginfo(landing_data, orginfo)
        images_to_reload = dict()
        if not landing_data.get("cover") and orginfo.cover:
            images_to_reload["cover"] = orginfo.cover
        if not landing_data.get("logo") and orginfo.logo:
            images_to_reload["logo"] = orginfo.logo

        buttons = (await self._suggester.cta_button(biz_id, landing_data, orginfo))[
            "available_buttons"
        ]
        if buttons:
            landing_data["preferences"]["cta_button"] = buttons[0]

        landing_data["blocks_options"] = {
            "show_cover": landing_data.get("cover") is not None,
            "show_logo": landing_data.get("logo") is not None,
            "show_schedule": True,
            "show_photos": True,
            "show_map_and_address": True,
            "show_services": True,
            "show_reviews": True,
            "show_extras": bool(landing_data["extras"]["plain_extras"]),
        }

        if biz_state is None:
            slug = await self._slug_maker(org_info=orginfo)
            # use head permalink
            await self._dm.create_biz_state(
                biz_id=biz_id, slug=slug, permalink=orginfo.permalink
            )
        else:
            slug = biz_state["slug"]
            if biz_state["permalink"] != orginfo.permalink:
                await self._dm.update_biz_state_permalink(
                    biz_id=biz_id, permalink=orginfo.permalink
                )

        landing_data["contacts"].pop("geo")

        logging.getLogger(__name__).info(
            f"[GEN_LAND] Generated landing data {landing_data}"
        )

        await self._dm.save_landing_data_for_biz_id(
            biz_id=biz_id, landing_data=landing_data, version=LandingVersion.STABLE
        )
        await self._dm.save_landing_data_for_biz_id(
            biz_id=biz_id, landing_data=landing_data, version=LandingVersion.UNSTABLE
        )
        if publish:
            await self._dm.set_landing_publicity(biz_id=biz_id, is_published=True)

        if images_to_reload:
            asyncio.create_task(
                self._retry_upload_images(
                    biz_id=biz_id, landing_details=landing_data, **images_to_reload
                )
            )

        return slug

    async def create_landing_from_data(
        self, permalink: int, name: str, categories: List[str], contacts: dict
    ) -> str:

        logging.getLogger(__name__).info(
            f"[CR_LAND] Generate landing permalink {permalink}"
        )

        biz_id = await self._bvm_client.fetch_biz_id_by_permalink(permalink=permalink)
        if biz_id is None:
            raise NoBizIdForOrg

        logging.getLogger(__name__).info(
            f"[CR_LAND] Found biz_id {biz_id} for permalink {permalink}"
        )

        biz_state = await self._dm.fetch_biz_state(biz_id=biz_id)
        logging.getLogger(__name__).info(
            f"[CR_LAND] Current biz state {biz_state} for biz_id {biz_id}"
        )

        if (
            biz_state is not None
            and biz_state["stable_version"] is not None
            and biz_state["unstable_version"] is not None
        ):
            return biz_state["slug"]

        landing_data = {
            "name": name,
            "categories": categories,
            "contacts": contacts,
            "extras": {"plain_extras": []},
            "preferences": {
                "color_theme": dict(self.default_color_theme),
            },
        }

        buttons = (await self._suggester.cta_button_by_data(permalink, landing_data))[
            "available_buttons"
        ]
        if buttons:
            landing_data["preferences"]["cta_button"] = buttons[0]

        landing_data["blocks_options"] = {
            "show_cover": True,
            "show_logo": True,
            "show_schedule": True,
            "show_photos": True,
            "show_map_and_address": True,
            "show_services": True,
            "show_reviews": True,
            "show_extras": True,
        }

        if biz_state is None:
            slug = await self._slug_maker(landing_data=landing_data)
            # use head permalink
            await self._dm.create_biz_state(
                biz_id=biz_id, slug=slug, permalink=str(permalink)
            )
        else:
            slug = biz_state["slug"]
            if biz_state["permalink"] != permalink:
                await self._dm.update_biz_state_permalink(
                    biz_id=biz_id, permalink=str(permalink)
                )

        logging.getLogger(__name__).info(
            f"[CR_LAND] Generated landing data {landing_data}"
        )

        await self._dm.save_landing_data_for_biz_id(
            biz_id=biz_id,
            landing_data=landing_data,
            version=LandingVersion.STABLE,
            updated_from_geosearch=True,
        )
        await self._dm.save_landing_data_for_biz_id(
            biz_id=biz_id,
            landing_data=landing_data,
            version=LandingVersion.UNSTABLE,
            updated_from_geosearch=True,
        )

        await self._dm.set_landing_publicity(biz_id=biz_id, is_published=True)

        return slug

    async def delete_landing_by_biz_id(self, biz_id: int) -> None:
        await self._dm.delete_landing_by_biz_id(biz_id=biz_id)

    async def update_biz_state_slug(self, biz_id: int, slug: str) -> None:
        return await self._dm.update_biz_state_slug(biz_id=biz_id, slug=slug)

    async def update_biz_state_set_blocked(
        self, biz_id: int, is_blocked: bool, blocking_data: Optional[dict] = None
    ) -> None:
        return await self._dm.update_biz_state_set_blocked(
            biz_id=biz_id, is_blocked=is_blocked, blocking_data=blocking_data
        )

    async def fetch_landing_data_for_crm(
        self, biz_id: int, version: LandingVersion
    ) -> dict:
        landing_data = await self._dm.fetch_landing_data_for_crm(
            biz_id=biz_id, version=version
        )

        orginfo = await self._geosearch_client.resolve_org(
            permalink=int(landing_data["landing_details"]["permalink"])
        )
        if not orginfo:
            is_updated_from_geosearch = landing_data["landing_details"].pop(
                "is_updated_from_geosearch", False
            )
            if not is_updated_from_geosearch:
                raise NoOrginfo
            return landing_data

        await self._enrich_geo_data(
            landing_data=landing_data["landing_details"], orginfo=orginfo
        )

        return landing_data

    def _combine_street_address(cls, orginfo: GeoSearchOrgResult) -> Optional[str]:
        if orginfo.address_components.get(
            AddressComponent.STREET
        ) and orginfo.address_components.get(AddressComponent.HOUSE):
            return (
                orginfo.address_components.get(AddressComponent.STREET)
                + ", "
                + orginfo.address_components.get(AddressComponent.HOUSE)
            )

        return None

    async def _geosearch_extract_geo(self, orginfo: GeoSearchOrgResult) -> None:
        geo_data = {
            "permalink": orginfo.permalink,
            "lat": orginfo.latitude,
            "lon": orginfo.longitude,
            "address": orginfo.formatted_address,
            # if address with house - consider it accurate
            "address_is_accurate": AddressComponent.HOUSE in orginfo.address_components,
            "country_code": orginfo.country_code,
            "postal_code": orginfo.postal_code,
            "address_region": orginfo.address_components.get(AddressComponent.PROVINCE),
            "street_address": self._combine_street_address(orginfo),
        }

        if AddressComponent.LOCALITY in orginfo.address_components:
            geo_data["locality"] = orginfo.address_components[AddressComponent.LOCALITY]

        if orginfo.service_area is not None:
            geo_data["service_area"] = await self._compose_service_area(
                org_info=orginfo
            )

        return geo_data

    async def _geosearch_extract_contacts(self, orginfo: GeoSearchOrgResult) -> None:
        contacts = {
            "geo": await self._geosearch_extract_geo(orginfo),
            "website": orginfo.own_links[0]
            if orginfo.own_links
            else None,  # just use 1st here
            "phone": orginfo.formatted_callable_phones[0]  # just use 1st here
            if orginfo.formatted_callable_phones
            else None,
            "phones": orginfo.formatted_callable_phones,
            "instagram": orginfo.social_links.get("instagram"),
            "facebook": orginfo.social_links.get("facebook"),
            "vkontakte": orginfo.social_links.get("vkontakte"),
            "twitter": orginfo.social_links.get("twitter"),
            "telegram": orginfo.social_links.get("telegram"),
            "viber": orginfo.social_links.get("viber"),
            "whatsapp": orginfo.social_links.get("whatsapp"),
            "email": orginfo.emails[0] if orginfo.emails else None,  # just use 1st here
        }

        return contacts

    @staticmethod
    def _geosearch_extract_schedule(org_data):
        if org_data.open_hours is not None:
            open_hours = org_data.metas["business"].open_hours
            schedule = {}
            if open_hours.HasField("tz_offset"):
                schedule["tz_offset"] = timedelta(seconds=open_hours.tz_offset)
            if open_hours.HasField("state") and open_hours.state.HasField("text"):
                schedule["work_now_text"] = open_hours.state.text

            schedule["schedule"] = list(
                {
                    "day": hours_pb2.DayOfWeek.Name(day),
                    "opens_at": 0
                    if time_range.HasField("all_day") and time_range.all_day
                    else getattr(time_range, "from"),  # noqa
                    "closes_at": 60 * 60 * 24
                    if time_range.HasField("all_day") and time_range.all_day
                    else time_range.to,
                }
                for hours in open_hours.hours
                for day in hours.day
                for time_range in hours.time_range
            )

            return schedule
        return None

    async def _enrich_geo_data(
        self, landing_data: dict, orginfo: GeoSearchOrgResult
    ) -> None:
        geo_data = await self._geosearch_extract_geo(orginfo)

        landing_data["contacts"]["geo"].update(geo_data)

    async def update_landing_data_from_crm(
        self, biz_id: int, version: LandingVersion, landing_details: dict
    ) -> dict:
        color_preset = landing_details["preferences"]["color_theme"]["preset"]
        if color_preset not in self.color_presets:
            raise UnknownColorPreset(color_preset)

        updated_details = await self._dm.save_landing_data_for_biz_id(
            biz_id=biz_id, version=version, landing_data=landing_details
        )
        landing_details = updated_details["landing_details"]

        orginfo = await self._geosearch_client.resolve_org(
            permalink=int(landing_details["permalink"])
        )
        if not orginfo:
            raise NoOrginfo

        if landing_details["permalink"] != orginfo.permalink:
            await self._dm.update_biz_state_permalink(
                biz_id=biz_id, permalink=orginfo.permalink
            )
            landing_details["permalink"] = orginfo.permalink
            landing_details["contacts"]["geo"]["permalink"] = orginfo.permalink

        await self._enrich_geo_data(landing_data=landing_details, orginfo=orginfo)

        if biz_state := await self._dm.fetch_biz_state(biz_id=biz_id):
            if biz_state["published"]:
                self._ext_feed_writer.write_urls_async(slug=biz_state["slug"])

        return updated_details

    def _tycoon_avatars_url(self, group_id: int, name: str) -> str:
        return f"{self._avatars_client.read_url}/get-tycoon/{group_id}/{name}/"

    @staticmethod
    def _insta_image_url(media) -> str:
        return media["preview_url"] if media["type"] == "VIDEO" else media["media_url"]

    async def _enrich_with_avatars(self, instagram: dict) -> None:
        source_urls = []
        for post in instagram["posts"]:
            for media in post["media_urls"]:
                if "internal_url" not in media:
                    source_urls.append(self._insta_image_url(media))

        if not source_urls:
            return
        avatars = await self._dm.fetch_avatars(source_urls)

        for post in instagram["posts"]:
            for media in post["media_urls"]:
                if "internal_url" not in media:
                    if avatars_id := avatars.get(self._insta_image_url(media)):
                        media["internal_url"] = self._tycoon_avatars_url(
                            avatars_id[0], avatars_id[1]
                        )

    def _is_simple_request(self, preferences: dict) -> bool:
        cta = preferences.get("cta_button")
        return (
            cta
            and cta.get("predefined") == "REQUEST"
            and cta.get("value", "").startswith(self._config["BASE_WIDGET_REQUEST_URL"])
        )

    @async_time_diff
    async def fetch_tuner_settings(self, biz_id: int) -> dict:
        try:
            return await self._tuner_client.fetch_settings(biz_id)
        except Exception as e:
            logging.getLogger(__name__).error(
                "Unable to fetch Tuner settings", exc_info=e
            )

    @async_time_diff
    async def fetch_published_landing_data_by_slug(
        self, slug: str, token: str, version: LandingVersion
    ) -> dict:
        if token != self._fetch_data_token:
            raise InvalidFetchToken
        logging.getLogger(__name__).info(f"[FETCH] slug={slug}")
        biz_state = await self._dm.fetch_biz_state_by_slug(slug=slug)
        if biz_state is None:
            raise NoDataForSlug

        data = await self._dm.fetch_landing_data_by_slug(slug=slug, version=version)
        if data is None:
            raise NoDataForSlug

        if version is LandingVersion.STABLE and not biz_state["published"]:
            return_200_for_unpublished = (
                await self._dm.fetch_cached_landing_config_feature(
                    Feature.RETURN_200_FOR_UNPUBLISHED
                )
            )
            if return_200_for_unpublished == "enabled":
                # need to return permalink and unpublished state to redirect to Ya.Maps
                return {
                    "name": data["name"],
                    "permalink": data["permalink"],
                    "published": False,
                    "contacts": {},
                    "preferences": {
                        "color_theme": {
                            "theme": "LIGHT",
                            "main_color_hex": "FFFFFF",
                            "text_color_over_main": "LIGHT",
                            "main_color_name": "WHITE",
                        }
                    },
                }

            raise NoDataForSlug

        if data["landing_type"] == "INSTAGRAM":
            await self._enrich_with_avatars(data["instagram"])
            if "geo" in data["contacts"] and "lat" not in data["contacts"]["geo"]:
                del data["contacts"]["geo"]
            return self._setup_color_scheme(data)

        use_loaded_geosearch_data: bool = (
            "enabled"
            == await self._dm.fetch_cached_landing_config_feature(
                Feature.USE_LOADED_GEOSEARCH_DATA
            )
        )

        # for preview we still fetch the most actual data
        is_updated_from_geosearch = data.pop("is_updated_from_geosearch", False)
        if (
            not use_loaded_geosearch_data
            or version is LandingVersion.UNSTABLE
            or not is_updated_from_geosearch
        ):
            with TimeDiff("geosearch_client.resolve_org"):
                geosearch_resp = await self._geosearch_client.resolve_org(
                    permalink=int(data["permalink"])
                )
            if geosearch_resp is None:
                raise NoOrginfo

            await self._refresh_permalink(data, geosearch_resp, biz_state)
            await self._update_with_geosearch_data(data, geosearch_resp)

        if version is LandingVersion.STABLE:
            await self._update_with_google_counters_data(data)
            await self._update_with_tiktok_pixels_data(data)
            await self._update_with_vk_pixels_data(data)

        self._setup_color_scheme(data)

        await self._setup_substitution_phone(data=data, biz_id=biz_state["biz_id"])

        data["promos"] = await self._dm.fetch_org_promos(biz_id=biz_state["biz_id"])

        promoted_cta = await self._dm.fetch_promoted_cta(biz_id=biz_state["biz_id"])
        if promoted_cta is not None and not re.match(
            f"^https?://{slug}\\.(tst-)?clients\\.site", promoted_cta["value"]
        ):
            data["preferences"]["cta_button"] = promoted_cta
        elif self._is_simple_request(data["preferences"]):
            logging.getLogger(__name__).info(
                "CTA SIMPLE_REQUEST biz_id %d", biz_state["biz_id"]
            )
            settings = await self.fetch_tuner_settings(biz_id=biz_state["biz_id"])
            if settings and settings.get("requests") and settings["requests"].enabled:
                data["preferences"]["cta_button"] = {
                    "custom": settings["requests"].button_text,
                    "value": data["preferences"]["cta_button"]["value"],
                }

        await self._respect_block_options(
            data=data,
            version=version,
            biz_id=biz_state["biz_id"],
            use_loaded_geosearch_data=use_loaded_geosearch_data,
        )

        data["blocked"] = biz_state.get("blocked")

        if (
            await self._dm.fetch_cached_landing_config_feature(Feature.USE_GOODS)
            == "enabled"
        ):
            data["goods"] = await self._dm.fetch_goods_data_for_permalink(
                int(data["permalink"])
            )

        data.pop("photo_settings", None)
        return data

    @async_time_diff
    async def _refresh_permalink(
        self, data: dict, geosearch_resp: GeoSearchOrgResult, biz_state: dict
    ):
        if (
            geosearch_resp.permalink_moved_to
            and biz_state["permalink"] != geosearch_resp.permalink_moved_to
        ):
            # get head of new permalink
            resp_with_head = await self._geosearch_client.resolve_org(
                permalink=int(geosearch_resp.permalink_moved_to)
            )

            await self._dm.update_biz_state_permalink(
                biz_id=biz_state["biz_id"], permalink=resp_with_head.permalink
            )
            data["permalink"] = resp_with_head.permalink

    @async_time_diff
    async def _setup_substitution_phone(self, data: dict, biz_id: int) -> None:
        substitution_phone = await self._dm.fetch_substitution_phone(biz_id=biz_id)
        if substitution_phone:
            data["contacts"]["phone"] = substitution_phone
            data["contacts"]["is_substitution_phone"] = True
            cta_button = data["preferences"].get("cta_button")
            if cta_button and cta_button.get("predefined") == "CALL":
                cta_button["value"] = substitution_phone
        elif "phone" in data["contacts"]:
            data["contacts"]["is_substitution_phone"] = False

    @async_time_diff
    async def _respect_block_options(
        self,
        data: dict,
        biz_id: int,
        version: LandingVersion,
        use_loaded_geosearch_data: bool,
    ) -> None:
        blocks_options = data.pop("blocks_options")

        if blocks_options["show_services"]:
            with TimeDiff("service_provider.fetch_services_info"):
                services = await self._service_provider.fetch_services_info(
                    biz_id=biz_id
                )
                if services:
                    data["services"] = dict(items=services)

        if blocks_options["show_reviews"]:
            rating_provider = RatingProvider(
                permalink=int(data["permalink"]),
                ugcdb_client=self._ugcdb_client,
            )
            with TimeDiff("rating_provider.fetch_rating"):
                rating = await rating_provider.fetch_rating(
                    reviews_min_rating=int(blocks_options["reviews_min_rating"])
                    if "reviews_min_rating" in blocks_options
                    else None
                )
            if rating is not None:
                data["rating"] = rating

        if use_loaded_geosearch_data:
            if blocks_options.get("show_branches") and data.get("chain_id") is not None:
                branches = await self._dm.fetch_branches_for_permalink(
                    data["permalink"], version, data["chain_id"]
                )
                for branch in branches:
                    self._setup_color_scheme(branch)
                data["branches"] = branches

        self._clear_blocks(data, blocks_options)

    @async_time_diff
    async def _update_with_geosearch_data(
        self, data: dict, geosearch_resp: GeoSearchOrgResult
    ) -> None:
        await self._enrich_geo_data(landing_data=data, orginfo=geosearch_resp)

        schedule = self._geosearch_extract_schedule(geosearch_resp)
        if schedule is not None:
            data["schedule"] = schedule

        data["photos"] = self._dm.filter_photos(
            photos=geosearch_resp.photos, settings=data.get("photo_settings") or {}
        )

        if geosearch_resp.metrika_counter is not None:
            data["preferences"][
                "external_metrika_code"
            ] = geosearch_resp.metrika_counter

    async def _compose_service_area(self, org_info: GeoSearchOrgResult) -> dict:
        if "service_radius_km" in org_info.service_area:
            return org_info.service_area
        elif org_info.service_area.get("geo_ids"):
            linguistics_for_regions = await asyncio.gather(
                *[
                    self._fetch_linguistics_for_region(
                        geo_id=geo_id, permalink=org_info.permalink
                    )
                    for geo_id in org_info.service_area["geo_ids"]
                ]
            )

            regions = []
            for linguistics in linguistics_for_regions:
                if linguistics:
                    regions.append(
                        {
                            "preposition": linguistics["preposition"],
                            "prepositional_case": linguistics["prepositional_case"],
                        }
                    )

            return {"area": {"regions": regions}}

    async def _fetch_linguistics_for_region(
        self, geo_id: int, permalink: str
    ) -> Optional[dict]:
        try:
            linguistics = await self._geobase_client.fetch_linguistics_for_region(
                geo_id=geo_id
            )

            return linguistics
        except Exception as exc:
            logging.getLogger("domain.geobase_fetch_linguistics").exception(
                f"Failed to fetch region name from GeoBase "
                f"for geo_id {geo_id} (permalink {permalink})",
                exc_info=exc,
            )

    def _setup_color_scheme(self, data: dict) -> dict:
        color_theme = data["preferences"]["color_theme"]
        preset = color_theme.pop("preset")

        if preset not in self.color_presets:
            logging.getLogger("domain.color_presets").error(
                "Unknown color preset %s, using default", preset
            )
            preset = self.default_preset

        color_theme.update(self.color_presets[preset])
        return data

    @async_time_diff
    async def _update_with_google_counters_data(self, data: dict) -> None:
        if (
            await self._dm.fetch_cached_landing_config_feature(
                Feature.USE_LOADED_GOOGLE_COUNTERS
            )
            == "enabled"
        ):
            data["preferences"][
                "google_counters"
            ] = await self._dm.fetch_google_counters_for_permalink(
                int(data["permalink"])
            )
        else:
            try:

                result = await self._yt_client.get_google_counters_for_permalink(
                    int(data["permalink"])
                )

                if result:
                    data["preferences"]["google_counters"] = result

            except Exception as e:
                logging.getLogger(__name__).error(
                    "Unable to obtain Google counters", exc_info=e
                )

    @async_time_diff
    async def _update_with_tiktok_pixels_data(self, data: dict) -> None:
        if (
            await self._dm.fetch_cached_landing_config_feature(
                Feature.USE_LOADED_TIKTOK_PIXELS
            )
            == "enabled"
        ):
            data["preferences"][
                "tiktok_pixels"
            ] = await self._dm.fetch_tiktok_pixels_for_permalink(int(data["permalink"]))

    @async_time_diff
    async def _update_with_vk_pixels_data(self, data: dict) -> None:
        vk_pixel = await self._dm.fetch_vk_pixels_for_permalink(int(data["permalink"]))
        if vk_pixel:
            data["preferences"]["vk_pixels"] = vk_pixel

    async def check_slug_is_free(self, slug: str, biz_id: Optional[int] = None) -> bool:
        if biz_id is None:
            return await self._dm.fetch_biz_state_by_slug(slug=slug) is None
        else:
            return await self._dm.check_slug_is_free(slug=slug, biz_id=biz_id)

    async def set_landing_publicity(self, biz_id: int, is_published: bool):
        biz_state = await self._dm.fetch_biz_state(biz_id=biz_id)
        if biz_state is None:
            raise NoDataForBizId

        if is_published and not biz_state["stable_version"]:
            raise NoStableVersionForPublishing

        if is_published != biz_state["published"]:
            await self._dm.set_landing_publicity(
                biz_id=biz_id, is_published=is_published
            )

    async def suggest_field_values(
        self, biz_id: int, field: str
    ) -> List[Union[str, Dict[str, Any]]]:
        return await self._suggester(biz_id=biz_id, field=field)

    async def _compose_landing_data_from_orginfo(
        self, landing_data: dict, orginfo: GeoSearchOrgResult
    ) -> None:
        logo_url, cover_url = await self._upload_images(
            logo=orginfo.logo,
            cover=orginfo.cover if orginfo.cover else None,
            timeout=self._avatars_upload_timeout,
        )

        if logo_url is not None:
            landing_data["logo"] = logo_url
        if cover_url is not None:
            landing_data["cover"] = cover_url

        if orginfo.formatted_callable_phones:
            landing_data["contacts"]["phone"] = orginfo.formatted_callable_phones[0]
        if orginfo.own_links:
            landing_data["contacts"]["website"] = orginfo.own_links[0]

        socials = {
            "instagram",
            "facebook",
            "vkontakte",
            "twitter",
            "telegram",
            "viber",
            "whatsapp",
        }
        for snet, link in orginfo.social_links.items():
            if snet in socials:
                landing_data["contacts"][snet] = link

        for feature in orginfo.features:
            if "name" in feature and feature["value"] is True:
                landing_data["extras"]["plain_extras"].append(
                    feature["name"].capitalize()
                )

    async def _retry_upload_images(
        self,
        *,
        biz_id: int,
        landing_details: dict,
        logo: Optional[str] = None,
        cover: Optional[str] = None,
    ) -> None:
        logo_url, cover_url = await self._upload_images(logo=logo, cover=cover)

        update_landing = False
        if logo_url is not None and not landing_details.get("logo"):
            landing_details["logo"] = logo_url
            update_landing = True
        if cover_url is not None and not landing_details.get("cover"):
            landing_details["cover"] = cover_url
            update_landing = True

        if update_landing:
            await self._dm.save_landing_data_for_biz_id(
                biz_id=biz_id,
                landing_data=landing_details,
                version=LandingVersion.STABLE,
            )
            await self._dm.save_landing_data_for_biz_id(
                biz_id=biz_id,
                landing_data=landing_details,
                version=LandingVersion.UNSTABLE,
            )

    async def _upload_images(
        self,
        *,
        timeout: Optional[int] = None,
        logo: Optional[str] = None,
        cover: Optional[str] = None,
    ) -> Tuple[Optional[str], Optional[str]]:
        logo_url, cover_url = None, None
        avatars_tasks = []
        if logo:
            avatars_tasks.append(
                self._avatars_client.put_image_by_url(
                    image_url=_tpl_postfix_re.sub("orig", logo), timeout=timeout
                )
            )
        else:
            avatars_tasks.append(async_noop())

        if cover:
            avatars_tasks.append(
                self._avatars_client.put_image_by_url(
                    image_url=_tpl_postfix_re.sub("orig", cover), timeout=timeout
                )
            )
        else:
            avatars_tasks.append(async_noop())

        logo_data, cover_data = await asyncio.gather(
            *avatars_tasks, return_exceptions=True
        )

        if logo_data is not None:
            if isinstance(logo_data, Exception):
                logging.getLogger("domain.images_upload").warning(
                    f"Failed to upload logo: {logo_data.__class__.__name__}"
                )
            else:
                logo_url = logo_data.url_template
        if cover_data is not None:
            if isinstance(cover_data, Exception):
                logging.getLogger("domain.images_upload").warning(
                    f"Failed to upload cover: {cover_data.__class__.__name__}"
                )
            else:
                cover_url = cover_data.url_template

        return logo_url, cover_url

    @staticmethod
    def _clear_blocks(data: dict, blocks_options: Dict[str, bool]) -> None:
        clearable_blocks = {
            "show_cover": "cover",
            "show_logo": "logo",
            "show_schedule": "schedule",
            "show_extras": "extras",
        }

        for option, block in clearable_blocks.items():
            if not blocks_options[option]:
                data.pop(block, None)

        if not blocks_options["show_photos"]:
            data["photos"] = []

        if not blocks_options["show_map_and_address"]:
            data["contacts"].pop("geo", None)

    async def get_landing_config(self, token: str) -> Optional[dict]:
        if token != self._fetch_data_token:
            raise InvalidFetchToken

        return await self._dm.fetch_cached_landing_config()

    async def update_landing_config(self) -> None:
        try:
            data = await self._bunker_client.get_node_content(
                self._landing_config_bunker_node,
                self._landing_config_bunker_node_version,
            )
            await self._dm.set_cached_landing_config(data)
        except Exception:
            logging.exception("Landing config update failure.")
            raise

    @async_time_diff
    async def edit_instagram_landing(
        self,
        biz_id: int,
        permalink: Optional[int] = None,
        instagram: Optional[dict] = None,
        cta_button: Optional[dict] = None,
        settings: Optional[dict] = None,
        contacts: Optional[dict] = None,
        social_buttons: Optional[List[dict]] = None,
    ) -> dict:
        logging.getLogger(__name__).info(
            "edit_instagram_landing %d permalink %s", biz_id, str(permalink)
        )
        biz_state = await self._dm.fetch_biz_state(biz_id=biz_id)
        preferences = {
            "color_theme": dict(self.default_color_theme),
        }
        if cta_button:
            preferences["cta_button"] = cta_button
        if social_buttons is not None:
            preferences["social_buttons"] = social_buttons

        if biz_state:
            landing_id = biz_state["stable_version"]
            await self._dm.save_instagram_landing(
                landing_id,
                instagram,
                preferences if cta_button or social_buttons is not None else None,
                settings,
                contacts,
            )
            if permalink is not None:
                await self._dm.update_biz_state_permalink(
                    biz_id=biz_id, permalink=str(permalink)
                )
            if biz_state["published"]:
                self._ext_feed_writer.write_urls_async(slug=biz_state["slug"])
            return {"slug": biz_state["slug"]}

        if not instagram:
            raise NoDataForBizId

        async def create_landing(slug: str) -> bool:
            return await self._dm.create_instagram_landing(
                biz_id,
                permalink or 0,
                slug,
                instagram,
                preferences,
                settings or {},
                contacts or {},
            )

        templates = ["insta-{}", "{}-insta", f"{{}}-{int(time.time())}"]
        slug = await SlugMaker(create_landing).make_slug(
            instagram["instagram_account"], templates
        )

        self._ext_feed_writer.write_urls_async(slug=slug)

        return {"slug": slug}

    async def fetch_landing_phone(self, permalink: int) -> dict:
        result = await self._dm.fetch_landing_phone(str(permalink))
        return {"phone": result}

    async def update_geosearch_data(self) -> None:
        logging.getLogger(__name__).info("UPDATE GEOSEARCH DATA")
        load_geosearch_data = await self._dm.fetch_cached_landing_config_feature(
            Feature.LOAD_GEOSEARCH_DATA
        )
        if load_geosearch_data != "enabled":
            return

        db_offset = 0
        db_limit = 10

        while True:
            permalinks = await self._dm.fetch_all_published_permalinks(
                offset=db_offset, limit=db_limit
            )
            if not permalinks:
                return

            logging.getLogger(__name__).info(f"UPDATE GEOSEARCH DATA permalinks {permalinks} offset {db_offset}")

            orgs_data = await self._geosearch_client.resolve_orgs(permalinks)

            for org_data in orgs_data:
                await self._dm.update_landing_data_with_geosearch(
                    permalink=org_data.permalink,
                    chain_id=int(org_data.chain_id)
                    if org_data.chain_id is not None
                    else None,
                    contacts=await self._geosearch_extract_contacts(org_data),
                    schedule=self._geosearch_extract_schedule(org_data),
                    photos=org_data.photos,
                    metrika_counter=org_data.metrika_counter,
                )
                await self._dm.update_instagram_landing_data_with_geosearch(
                    permalink=org_data.permalink,
                    geo=await self._geosearch_extract_geo(org_data),
                    metrika_counter=org_data.metrika_counter,
                )

                if (
                    org_data.permalink_moved_to is not None
                    and org_data.permalink_moved_to != org_data.permalink
                ):
                    logging.getLogger(__name__).info(
                        f"UPDATE GEOSEARCH DATA permalink {org_data.permalink} moved to {org_data.permalink_moved_to}"
                    )
                    await self._dm.update_permalink_from_geosearch(
                        old_permalink=org_data.permalink,
                        new_permalink=org_data.permalink_moved_to,
                    )

            db_offset += db_limit

    async def push_landing_urls_to_ext_feed_lb(self):
        """
        Process all landing urls and send them to Robot LB for indexing.
        TODO: minimize the number of pushed urls by excluding those that have not been changed since the last task run.
        """
        db_offset = 0
        db_limit = 1000

        while True:
            slugs = await self._dm.fetch_published_slugs(
                offset=db_offset, limit=db_limit
            )
            if not slugs:
                return

            await self._ext_feed_writer.write_urls(slugs=slugs)
            logging.info(
                f"Attempted to send {db_offset + len(slugs)} landing urls in total to Robot's ext feed LB"
            )

            db_offset += db_limit

    async def fetch_landing_photos(
        self, biz_id: int, version: LandingVersion
    ) -> List[dict]:
        return {"photos": await self._dm.fetch_landing_photos(biz_id, version)}

    async def hide_landing_photos(
        self, biz_id: int, version: LandingVersion, photo_id_to_hidden: Dict[str, bool]
    ) -> None:
        await self._dm.hide_landing_photos(biz_id, version, photo_id_to_hidden)

    async def get_landing_slug(self, permalink: int) -> Dict[str, str]:

        try:
            biz_id = await self._bvm_client.fetch_biz_id_no_create_by_permalink(
                permalink
            )
            if biz_id is None:
                raise NoBizIdForOrg

        except Exception:
            raise NoBizIdForOrg

        biz_state = await self._dm.fetch_biz_state(biz_id=biz_id)

        if biz_state is None or not biz_state["published"]:
            raise NoDataForBizId

        return biz_state["slug"]
