import dataclasses
import textwrap
from typing import (
    Optional,
    Union,
    Any,
    Self,
    TYPE_CHECKING,
)
from collections.abc import Mapping, Sequence, Callable, Iterable

from debputy.lsp.named_styles import ALL_PUBLIC_NAMED_STYLES
from debputy.lsp.ref_models.deb822_reference_parse_models import UsageHint
from debian._deb822_repro import Deb822ParagraphElement

if TYPE_CHECKING:
    import lsprotocol.types as types
    from debputy.linting.lint_util import LintState
    from debputy.lsp.debputy_ls import DebputyLanguageServer
else:
    import debputy.lsprotocol.types as types

LSP_DATA_DOMAIN = "debputy-lsp-data"


def format_comp_item_synopsis_doc(
    usage_hint: UsageHint | None,
    synopsis_doc: str | None,
    is_deprecated: bool,
) -> str:
    if is_deprecated:
        return (
            f"[OBSOLETE]: {synopsis_doc}"
            if synopsis_doc is not None and not synopsis_doc.isspace()
            else f"[OBSOLETE]"
        )
    if usage_hint is not None:
        return (
            f"[{usage_hint.upper()}]: {synopsis_doc}"
            if synopsis_doc is not None and not synopsis_doc.isspace()
            else f"[{usage_hint.upper()}]"
        )
    return synopsis_doc or ""


@dataclasses.dataclass(slots=True, frozen=True)
class Keyword:
    value: str
    synopsis: str | None = None
    long_description: str | None = None
    translation_context: str = ""
    is_obsolete: bool = False
    replaced_by: str | None = None
    is_exclusive: bool = False
    """For keywords in fields that allow multiple keywords, the `is_exclusive` can be
    used for keywords that cannot be used with other keywords. As an example, the `all`
    value in `Architecture` of `debian/control` cannot be used with any other architecture.
    """
    is_alias_of: str | None = None
    is_completion_suggestion: bool = True
    sort_text: None | (
        str
        | Callable[["Keyword", "LintState", Sequence[Deb822ParagraphElement], str], str]
    ) = None
    usage_hint: UsageHint | None = None
    can_complete_keyword_in_stanza: None | (
        Callable[[Iterable[Deb822ParagraphElement]], bool]
    ) = None

    @property
    def is_deprecated(self) -> bool:
        return self.is_obsolete or self.replaced_by is not None

    def resolve_sort_text(
        self,
        lint_state: "LintState",
        stanza_parts: Sequence[Deb822ParagraphElement],
        value_being_completed: str,
    ) -> str | None:
        sort_text = self.sort_text
        if sort_text is None:
            return None
        if not isinstance(sort_text, str):
            return sort_text(
                self,
                lint_state,
                stanza_parts,
                value_being_completed,
            )
        return sort_text

    def is_keyword_valid_completion_in_stanza(
        self,
        stanza_parts: Sequence[Deb822ParagraphElement],
    ) -> bool:
        return (
            self.can_complete_keyword_in_stanza is None
            or self.can_complete_keyword_in_stanza(stanza_parts)
        )

    def replace(self, **changes: Any) -> "Self":
        return dataclasses.replace(self, **changes)

    def synopsis_translated(
        self, translation_provider: Union["DebputyLanguageServer", "LintState"]
    ) -> str | None:
        if self.synopsis is None:
            return None
        return translation_provider.translation(LSP_DATA_DOMAIN).pgettext(
            self.translation_context,
            self.synopsis,
        )

    def long_description_translated(
        self, translation_provider: Union["DebputyLanguageServer", "LintState"]
    ) -> str | None:
        if self.long_description is None:
            return None
        return translation_provider.translation(LSP_DATA_DOMAIN).pgettext(
            self.translation_context,
            self.long_description,
        )

    def as_completion_item(
        self,
        lint_state: "LintState",
        stanza_parts: Sequence[Deb822ParagraphElement],
        value_being_completed: str,
        markdown_kind: types.MarkupKind,
    ) -> types.CompletionItem:
        return types.CompletionItem(
            self.value,
            insert_text=self.value if self.is_alias_of is None else self.is_alias_of,
            sort_text=self.resolve_sort_text(
                lint_state,
                stanza_parts,
                value_being_completed,
            ),
            detail=format_comp_item_synopsis_doc(
                self.usage_hint,
                self.synopsis_translated(lint_state),
                self.is_deprecated,
            ),
            deprecated=self.is_deprecated,
            tags=[types.CompletionItemTag.Deprecated] if self.is_deprecated else None,
            documentation=(
                types.MarkupContent(value=self.long_description, kind=markdown_kind)
                if self.long_description
                else None
            ),
        )


def allowed_values(values: Iterable[str | Keyword]) -> Mapping[str, Keyword]:
    as_keywords = [k if isinstance(k, Keyword) else Keyword(k) for k in values]
    as_mapping = {k.value: k for k in as_keywords if k.value}
    # Simple bug check
    assert len(as_keywords) == len(as_mapping)
    return as_mapping


# This is the set of styles that `debputy` explicitly supports, which is more narrow than
# the ones in the config file.
ALL_PUBLIC_NAMED_STYLES_AS_KEYWORDS = allowed_values(
    Keyword(s.name, long_description=s.long_description)
    for s in ALL_PUBLIC_NAMED_STYLES.values()
)
