import re
import sys
from enum import Enum
from typing import (
    NotRequired,
    List,
    Any,
    Optional,
    Literal,
    Dict,
    Union,
)
from collections.abc import Iterable

from debputy.lsp.diagnostics import LintSeverity
from debputy.lsp.vendoring._deb822_repro import (
    LIST_SPACE_SEPARATED_INTERPRETATION,
    LIST_COMMA_SEPARATED_INTERPRETATION,
)
from debputy.lsp.vendoring._deb822_repro.parsing import (
    LIST_UPLOADERS_INTERPRETATION,
    Deb822ParsedTokenList,
    Interpretation,
    _parse_whitespace_list_value,
    Deb822ParsedValueElement,
    _parsed_value_render_factory,
    ListInterpretation,
    _parse_separator_list_value,
)
from debputy.lsp.vendoring._deb822_repro.tokens import (
    Deb822SpaceSeparatorToken,
    _value_line_tokenizer,
    _RE_WHITESPACE_SEPARATED_WORD_LIST,
    Deb822ValueToken,
    Deb822Token,
    Deb822SeparatorToken,
    Deb822WhitespaceToken,
)
from debputy.manifest_parser.declarative_parser import ParserGenerator
from debputy.manifest_parser.tagging_types import DebputyParsedContent

_DEB822_REFERENCE_DATA_PARSER_GENERATOR = ParserGenerator()

# FIXME: should go into python3-debian
_RE_COMMA = re.compile("([^,]*),([^,]*)")

UsageHint = Literal["rare",]


@_value_line_tokenizer
def comma_or_space_split_tokenizer(v: str) -> Iterable[Deb822Token]:
    assert "\n" not in v
    for match in _RE_WHITESPACE_SEPARATED_WORD_LIST.finditer(v):
        space_before, word, space_after = match.groups()
        if space_before:
            yield Deb822SpaceSeparatorToken(sys.intern(space_before))
        if "," in word:
            for m in _RE_COMMA.finditer(word):
                word_before, word_after = m.groups()
                if word_before:
                    yield Deb822ValueToken(word_before)
                # ... not quite a whitespace, but it is too much pain to make it a non-whitespace token.
                yield Deb822SpaceSeparatorToken(",")
                if word_after:
                    yield Deb822ValueToken(word_after)
        else:
            yield Deb822ValueToken(word)
        if space_after:
            yield Deb822SpaceSeparatorToken(sys.intern(space_after))


# FIXME: should go into python3-debian
LIST_COMMA_OR_SPACE_SEPARATED_INTERPRETATION = ListInterpretation(
    comma_or_space_split_tokenizer,
    _parse_whitespace_list_value,
    Deb822ParsedValueElement,
    Deb822SpaceSeparatorToken,
    lambda: Deb822SpaceSeparatorToken(","),
    _parsed_value_render_factory,
)


class Deb822SemicolonToken(Deb822SeparatorToken):
    """Used by the semicolon-separated list value parsers to denote a semicolon between two value tokens."""

    __slots__ = ()

    def __init__(self):
        # type: () -> None
        super().__init__(";")


_RE_SEMICOLON_SEPARATED_WORD_LIST = re.compile(
    r"""
    # This regex is slightly complicated by the fact that it should work with
    # finditer and consume the entire value.
    #
    # To do this, we structure the regex so it always starts on a separator (except
    # for the first iteration, where we permit the absence of a separator)

    (?:                                      # Optional space followed by a mandatory separator unless
                                             # it is the start of the "line" (in which case, we
                                             # allow the comma to be omitted)
        ^
        |
        (?:
            (?P<space_before_separator>\s*)  # This space only occurs in practise if the line
                                             # starts with space + separator.
            (?P<separator> ;)
        )
    )

    # From here it is "optional space, maybe a word and then optional space" again.  One reason why
    # all of it is optional is to gracefully cope with trailing separator.
    (?P<space_before_word>\s*)
    (?P<word> [^,\s] (?: [^;]*[^;\s])? )?    # "Words" can contain spaces for the separated list.
                                             # But surrounding whitespace is ignored
    (?P<space_after_word>\s*)
""",
    re.VERBOSE,
)


@_value_line_tokenizer
def comma_split_tokenizer(v):
    # type: (str) -> Iterable[Deb822Token]
    assert "\n" not in v
    for match in _RE_SEMICOLON_SEPARATED_WORD_LIST.finditer(v):
        space_before_comma, comma, space_before_word, word, space_after_word = (
            match.groups()
        )
        if space_before_comma:
            yield Deb822WhitespaceToken(sys.intern(space_before_comma))
        if comma:
            yield Deb822SemicolonToken()
        if space_before_word:
            yield Deb822WhitespaceToken(sys.intern(space_before_word))
        if word:
            yield Deb822ValueToken(word)
        if space_after_word:
            yield Deb822WhitespaceToken(sys.intern(space_after_word))


_parse_semicolon_list_value = _parse_separator_list_value(
    lambda x: isinstance(x, Deb822SemicolonToken)
)


LIST_SEMICOLON_SEPARATED_INTERPRETATION = ListInterpretation(
    comma_split_tokenizer,
    _parse_semicolon_list_value,
    Deb822ParsedValueElement,
    Deb822SemicolonToken,
    Deb822SemicolonToken,
    _parsed_value_render_factory,
)

_KEY2FIELD_VALUE_CLASS: dict[str, "FieldValueClass"]


class FieldValueClass(Enum):
    SINGLE_VALUE = "single-value", LIST_SPACE_SEPARATED_INTERPRETATION
    SPACE_SEPARATED_LIST = "space-separated-list", LIST_SPACE_SEPARATED_INTERPRETATION
    BUILD_PROFILES_LIST = "build-profiles-list", None  # TODO
    COMMA_SEPARATED_LIST = "comma-separated-list", LIST_COMMA_SEPARATED_INTERPRETATION
    SEMICOLON_SEPARATED_LIST = (
        "semicolon-separated-list",
        LIST_SEMICOLON_SEPARATED_INTERPRETATION,
    )
    COMMA_SEPARATED_EMAIL_LIST = (
        "comma-separated-email-list",
        LIST_UPLOADERS_INTERPRETATION,
    )
    COMMA_OR_SPACE_SEPARATED_LIST = (
        "comma-or-space-separated-list",
        LIST_COMMA_OR_SPACE_SEPARATED_INTERPRETATION,
    )
    FREE_TEXT_FIELD = "free-text", None
    DEP5_FILE_LIST = "dep5-file-list", LIST_SPACE_SEPARATED_INTERPRETATION

    @classmethod
    def from_key(cls, key: str) -> "FieldValueClass":
        return _KEY2FIELD_VALUE_CLASS[key]

    @property
    def key(self) -> str:
        return self.value[0]

    def interpreter(self) -> Interpretation[Deb822ParsedTokenList[Any, Any]] | None:
        return self.value[1]


# TODO: Have the parser generator support enums better than this hack.
FieldValueType = Literal[tuple(x.key for x in FieldValueClass)]
_KEY2FIELD_VALUE_CLASS = {x.key: x for x in FieldValueClass}


class Documentation(DebputyParsedContent):
    synopsis: NotRequired[str]
    long_description: NotRequired[str]
    uris: NotRequired[list[str]]


class GenericVariable(DebputyParsedContent):
    name: str
    documentation: NotRequired[Documentation]


class DCtrlSubstvar(GenericVariable):
    defined_by: str
    dh_sequence: NotRequired[str]


class GenericVariablesReferenceData(DebputyParsedContent):
    variables: list[GenericVariable]


class DctrlSubstvarsReferenceData(DebputyParsedContent):
    variables: list[DCtrlSubstvar]


class Alias(DebputyParsedContent):
    alias: str
    is_completion_suggestion: NotRequired[Literal[True]]


class StaticValue(DebputyParsedContent):
    value: str
    documentation: NotRequired[Documentation]
    sort_key: NotRequired[str]
    is_exclusive: NotRequired[Literal[True]]
    usage_hint: NotRequired[UsageHint]
    aliases: NotRequired[list[Alias]]


class Deb822Field(DebputyParsedContent):
    canonical_name: str
    field_value_type: FieldValueType
    unknown_value_authority: NotRequired[str]
    unknown_value_severity: NotRequired[LintSeverity | Literal["none"]]
    missing_field_authority: NotRequired[str]
    missing_field_severity: NotRequired[LintSeverity]
    default_value: NotRequired[str]
    warn_if_default: NotRequired[bool]
    usage_hint: NotRequired[UsageHint]
    documentation: NotRequired[Documentation]
    values: NotRequired[list[StaticValue]]
    replaced_by: NotRequired[str]
    is_obsolete_without_replacement: NotRequired[Literal[True]]
    spellcheck_value: NotRequired[Literal[True]]
    supports_substvars: NotRequired[Literal[False]]
    aliases: NotRequired[list[Alias]]
    inheritable_from_other_stanza: NotRequired[Literal[True]]


class StanzaType(DebputyParsedContent):
    stanza_name: str
    fields: list[Deb822Field]


class ReferenceVariable(DebputyParsedContent):
    name: str
    fallback: str


class ReferenceDefinition(DebputyParsedContent):
    variables: NotRequired[list[ReferenceVariable]]


class Deb822ReferenceData(DebputyParsedContent):
    definitions: NotRequired[ReferenceDefinition]
    stanza_types: list[StanzaType]


DEB822_REFERENCE_DATA_PARSER = _DEB822_REFERENCE_DATA_PARSER_GENERATOR.generate_parser(
    Deb822ReferenceData
)


GENERIC_VARIABLE_REFERENCE_DATA_PARSER = (
    _DEB822_REFERENCE_DATA_PARSER_GENERATOR.generate_parser(
        GenericVariablesReferenceData
    )
)


DCTRL_SUBSTVARS_REFERENCE_DATA_PARSER = (
    _DEB822_REFERENCE_DATA_PARSER_GENERATOR.generate_parser(DctrlSubstvarsReferenceData)
)
