from html.parser import HTMLParser

import structlog
from sqlalchemy.ext.asyncio import AsyncSession

from app.config import settings
from app.database.crud.public_offer import (
    get_public_offer,
    set_public_offer_enabled,
    upsert_public_offer,
)
from app.database.models import PublicOffer


logger = structlog.get_logger(__name__)


class PublicOfferService:
    """Helpers for managing the public offer text and visibility."""

    MAX_PAGE_LENGTH = 3500

    @staticmethod
    def _normalize_language(language: str) -> str:
        base_language = language or settings.DEFAULT_LANGUAGE or 'ru'
        return base_language.split('-')[0].lower()

    @staticmethod
    def normalize_language(language: str) -> str:
        return PublicOfferService._normalize_language(language)

    @classmethod
    async def get_offer(
        cls,
        db: AsyncSession,
        language: str,
        *,
        fallback: bool = False,
    ) -> PublicOffer | None:
        lang = cls._normalize_language(language)
        offer = await get_public_offer(db, lang)

        if offer or not fallback:
            return offer

        default_lang = cls._normalize_language(settings.DEFAULT_LANGUAGE)
        if lang != default_lang:
            return await get_public_offer(db, default_lang)

        return offer

    @classmethod
    async def get_active_offer(
        cls,
        db: AsyncSession,
        language: str,
    ) -> PublicOffer | None:
        lang = cls._normalize_language(language)
        offer = await get_public_offer(db, lang)

        if offer:
            if offer.is_enabled and offer.content.strip():
                return offer

            if not offer.is_enabled:
                return None

        default_lang = cls._normalize_language(settings.DEFAULT_LANGUAGE)
        if lang != default_lang:
            fallback_offer = await get_public_offer(db, default_lang)
            if fallback_offer and fallback_offer.is_enabled and fallback_offer.content.strip():
                return fallback_offer

        return None

    @classmethod
    async def is_offer_enabled(cls, db: AsyncSession, language: str) -> bool:
        offer = await cls.get_active_offer(db, language)
        return offer is not None

    @classmethod
    async def save_offer(
        cls,
        db: AsyncSession,
        language: str,
        content: str,
    ) -> PublicOffer:
        lang = cls._normalize_language(language)
        enable_if_new = True
        offer = await upsert_public_offer(
            db,
            lang,
            content,
            enable_if_new=enable_if_new,
        )
        logger.info('✅ Публичная оферта обновлена для языка', lang=lang)
        return offer

    @classmethod
    async def set_enabled(
        cls,
        db: AsyncSession,
        language: str,
        enabled: bool,
    ) -> PublicOffer:
        lang = cls._normalize_language(language)
        return await set_public_offer_enabled(db, lang, enabled)

    @classmethod
    async def toggle_enabled(
        cls,
        db: AsyncSession,
        language: str,
    ) -> PublicOffer:
        lang = cls._normalize_language(language)
        offer = await get_public_offer(db, lang)

        if offer:
            new_status = not offer.is_enabled
        else:
            new_status = True

        return await set_public_offer_enabled(db, lang, new_status)

    class _RichTextPaginator(HTMLParser):
        """Split HTML-like text into Telegram-safe chunks."""

        SELF_CLOSING_TAGS = {'br', 'hr', 'img', 'input', 'meta', 'link'}

        def __init__(self, max_len: int) -> None:
            super().__init__(convert_charrefs=False)
            self.max_len = max_len
            self.pages: list[str] = []
            self.current_parts: list[str] = []
            self.current_length = 0
            self.open_stack: list[tuple[str, str, int]] = []
            self.closing_length = 0
            self.needs_prefix = False
            self.prefix_length = 0

        def _closing_sequence(self) -> str:
            return ''.join(f'</{name}>' for name, _, _ in reversed(self.open_stack))

        def _ensure_prefix(self) -> None:
            if not self.needs_prefix:
                return

            prefix = ''.join(token for _, token, _ in self.open_stack)
            if prefix:
                self.current_parts.append(prefix)
                self.current_length += len(prefix)
            self.needs_prefix = False

        def _flush(self) -> None:
            if not self.current_parts and not self.closing_length:
                return

            content = ''.join(self.current_parts)
            closing_tags = self._closing_sequence()
            page = (content + closing_tags).strip()
            if page:
                self.pages.append(page)

            self.current_parts = []
            self.current_length = 0
            if self.prefix_length + self.closing_length > self.max_len:
                self.open_stack = []
                self.closing_length = 0
                self.prefix_length = 0
                self.needs_prefix = False
            else:
                self.needs_prefix = bool(self.open_stack)

        @staticmethod
        def _format_attrs(attrs: list[tuple[str, str | None]]) -> str:
            parts = []
            for name, value in attrs:
                if value is None:
                    parts.append(f' {name}')
                else:
                    parts.append(f' {name}="{value}"')
            return ''.join(parts)

        def _append_token(self, token: str) -> None:
            while True:
                self._ensure_prefix()
                if self.current_length + len(token) + self.closing_length <= self.max_len:
                    self.current_parts.append(token)
                    self.current_length += len(token)
                    break

                self._flush()

                if len(token) > self.max_len and not self.current_parts:
                    self.current_parts.append(token)
                    self.current_length += len(token)
                    break

        def handle_data(self, data: str) -> None:
            if not data:
                return

            remaining = data
            while remaining:
                self._ensure_prefix()
                available = self.max_len - (self.current_length + self.closing_length)
                if available <= 0:
                    self._flush()
                    continue

                piece = remaining[:available]
                self.current_parts.append(piece)
                self.current_length += len(piece)
                remaining = remaining[available:]

                if remaining:
                    self._flush()

        def handle_entityref(self, name: str) -> None:
            self.handle_data(f'&{name};')

        def handle_charref(self, name: str) -> None:
            self.handle_data(f'&#{name};')

        def _handle_self_closing(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
            token = f'<{tag}{self._format_attrs(attrs)}/>'
            self._append_token(token)

        def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
            if tag in self.SELF_CLOSING_TAGS:
                self._handle_self_closing(tag, attrs)
                return

            token = f'<{tag}{self._format_attrs(attrs)}>'
            closing_token = f'</{tag}>'
            closing_len = len(closing_token)

            while True:
                self._ensure_prefix()
                projected_length = self.current_length + len(token) + self.closing_length + closing_len
                if projected_length <= self.max_len:
                    self.current_parts.append(token)
                    self.current_length += len(token)
                    self.open_stack.append((tag, token, closing_len))
                    self.closing_length += closing_len
                    self.prefix_length += len(token)
                    break

                self._flush()

                if len(token) + closing_len > self.max_len and not self.current_parts:
                    self.current_parts.append(token)
                    self.current_length += len(token)
                    self.open_stack.append((tag, token, closing_len))
                    self.closing_length += closing_len
                    self.prefix_length += len(token)
                    break

        def handle_endtag(self, tag: str) -> None:
            token = f'</{tag}>'
            closing_len_reduction = 0
            index_to_remove = None
            for index in range(len(self.open_stack) - 1, -1, -1):
                if self.open_stack[index][0] == tag:
                    closing_len_reduction = self.open_stack[index][2]
                    index_to_remove = index
                    break

            while True:
                self._ensure_prefix()
                projected_closing_length = self.closing_length - closing_len_reduction
                projected_closing_length = max(projected_closing_length, 0)

                projected_total = self.current_length + len(token) + projected_closing_length
                if projected_total <= self.max_len or not self.current_parts:
                    self.current_parts.append(token)
                    self.current_length += len(token)
                    if index_to_remove is not None:
                        removed_tag = self.open_stack.pop(index_to_remove)
                        self.closing_length -= closing_len_reduction
                        self.prefix_length -= len(removed_tag[1])
                    break

                self._flush()

        def handle_startendtag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
            self._handle_self_closing(tag, attrs)

        def finalize(self) -> list[str]:
            self._flush()
            return self.pages

    @classmethod
    def _split_rich_paragraph(cls, paragraph: str, max_len: int) -> list[str]:
        if len(paragraph) <= max_len:
            return [paragraph]

        paginator = cls._RichTextPaginator(max_len)
        paginator.feed(paragraph)
        paginator.close()
        pages = paginator.finalize()
        return pages or [paragraph]

    @classmethod
    def split_content_into_pages(
        cls,
        content: str,
        *,
        max_length: int = None,
    ) -> list[str]:
        if not content:
            return []

        normalized = content.replace('\r\n', '\n').strip()
        if not normalized:
            return []

        max_len = max_length or cls.MAX_PAGE_LENGTH

        if len(normalized) <= max_len:
            return [normalized]

        paragraphs = [paragraph.strip() for paragraph in normalized.split('\n\n') if paragraph.strip()]

        pages: list[str] = []
        current = ''

        def flush_current() -> None:
            nonlocal current
            if current:
                pages.append(current.strip())
                current = ''

        for paragraph in paragraphs:
            segments = cls._split_rich_paragraph(paragraph, max_len)
            for segment in segments:
                segment = segment.strip()
                if not segment:
                    continue

                candidate = f'{current}\n\n{segment}'.strip() if current else segment
                if len(candidate) <= max_len:
                    current = candidate
                    continue

                flush_current()
                current = segment

        flush_current()

        if not pages:
            return [normalized[:max_len]]

        return pages
