import enum
from typing import Annotated, Literal

import pydantic

from ffun.core.entities import BaseEntity
from ffun.domain.entities import TagUid, TagUidPart


# Tag categories are required for:
# - tag normalization (define mutability and removability of a tag)
# - GUI (potentially in the future)
# - analytics (potentially in the future)
class TagCategory(enum.StrEnum):
    # represents a network domain, for example, the domain of a website whose feed is tagged
    network_domain = "network-domain"

    # represents a native feed tag, i.e. tags that were assigned by the author of the feed
    feed_tag = "feed-tag"

    # represents a tag with special meaning
    # most likely this tag is defined by a specific tag processor that checks
    # versy specific conditions, for example, @upper-case-title
    special = "special"

    # represents a tag that is assigned by some non-deterministic processor
    # for example, tags generated by LLMs
    free_form = "free-form"

    # Tags with this category created for testing purposes, most-likely by automated tests
    test_final = "test-final"
    test_raw = "test-raw"
    test_preserve = "test-preserve"


TagCategories = Annotated[
    set[TagCategory],
    # tag must have at least one category for proper normalization
    pydantic.Field(default_factory=set, min_length=1),
]


class NormalizationMode(enum.StrEnum):
    raw = "raw"  # tag should be processed by normalizers and can be removed
    preserve = "preserve"  # tag should be processed by normalizers but can not be removed
    final = "final"  # tag should not be processed by normalizers and can not be removed


class TagInNormalization(BaseEntity):
    uid: TagUid
    parts: list[TagUidPart]

    link: str | None
    categories: TagCategories

    # Here is going some complicated unclear logic:
    # - normalizers work with TagInNormalization
    # - TagInNormalization should define how it should be processed by normalizers
    # - There are two options: define explicitly or implicitly (derive from categories)
    # - It may look like a good idea to define it explicitly, so we could have a normalizer
    #   that could say "I produce this new tag which should be processed as raw/preserve/final"
    # - But this approach leads to uncertainty when we doing re-normalization of tags in the database
    #   because we don't store the final normalization mode in the database
    #   (and it may be wrong in case of re-normalization)
    #   So, on re-normalization we use tag categories to derive the mode (again)
    #   We also use RawTag, not TagInNormalization as a result of running a normalizer.
    # - That's why it seems more consistent to try building logic of normalizators around categories only
    #   To be consistent in the whole system
    # => We expect, that normalizer, if it requires, will be able to set new categories for the tags it produces
    #    For example, there may be a normalizer that detects network domains in free-form tags
    @property
    def mode(self) -> NormalizationMode:  # noqa: CCR001
        # The order of checks is important here

        if TagCategory.network_domain in self.categories:
            return NormalizationMode.final

        if TagCategory.special in self.categories:
            return NormalizationMode.final

        # We do not normalize native feed tags, because:
        # - We have no control over the logic that assigns them
        # - Sometimes they are (semi-)technical (special terms, domain names, codes)
        # - Sometimes they are very specific, like r-sideproject (for subreddits)
        #   and we don't want to create a duplicated tag like r-sideprojects that actually has no meaning
        if TagCategory.feed_tag in self.categories:
            return NormalizationMode.final

        if TagCategory.free_form in self.categories:
            return NormalizationMode.raw

        if TagCategory.test_final in self.categories:
            return NormalizationMode.final

        if TagCategory.test_preserve in self.categories:
            return NormalizationMode.preserve

        if TagCategory.test_raw in self.categories:
            return NormalizationMode.raw

        raise NotImplementedError(f"Tag with unknown categories: {self.categories}")


class NormalizerType(enum.StrEnum):
    fake = "fake"
    part_blacklist = "part_blacklist"
    part_replacer = "part_replacer"
    splitter = "splitter"
    form_normalizer = "form_normalizer"


class BaseNormalizer(BaseEntity):
    id: int
    enabled: bool
    name: str
    type: NormalizerType


class PartBlacklist(BaseNormalizer):
    type: Literal[NormalizerType.part_blacklist] = NormalizerType.part_blacklist
    blacklist: set[str] = pydantic.Field(default_factory=set)


class PartReplacer(BaseNormalizer):
    type: Literal[NormalizerType.part_replacer] = NormalizerType.part_replacer
    replacements: dict[str, str] = pydantic.Field(default_factory=dict)


class Splitter(BaseNormalizer):
    type: Literal[NormalizerType.splitter] = NormalizerType.splitter
    separators: set[str] = pydantic.Field(default_factory=set)


class FormNormalizer(BaseNormalizer):
    type: Literal[NormalizerType.form_normalizer] = NormalizerType.form_normalizer
    spacy_model: str
    cos_cache_size: int
    forms_cache_size: int


TagNormalizer = Annotated[
    PartBlacklist | PartReplacer | Splitter | FormNormalizer,
    pydantic.Field(discriminator="type"),
]


class NormalizersConfig(pydantic.BaseModel):
    tag_normalizer: tuple[TagNormalizer, ...]
