import dataclasses
import re
from typing import (
    Optional,
    Union,
    Sequence,
    Tuple,
    Set,
    Any,
    Container,
    List,
    Iterable,
    Iterator,
    Callable,
)

from debputy.lsp.debputy_ls import DebputyLanguageServer
from debputy.lsp.lsp_debian_control_reference_data import (
    Deb822FileMetadata,
    Deb822KnownField,
    StanzaMetadata,
    FieldValueClass,
    F,
    S,
)
from debputy.lsp.lsp_features import SEMANTIC_TOKEN_TYPES_IDS
from debputy.lsp.text_util import normalize_dctrl_field_name, te_position_to_lsp
from debputy.lsp.vendoring._deb822_repro import parse_deb822_file
from debputy.lsp.vendoring._deb822_repro.parsing import (
    Deb822KeyValuePairElement,
    LIST_SPACE_SEPARATED_INTERPRETATION,
    Deb822ParagraphElement,
    Deb822ValueLineElement,
)
from debputy.lsp.vendoring._deb822_repro.tokens import tokenize_deb822_file, Deb822Token
from debputy.util import _info
from lsprotocol.types import (
    CompletionParams,
    CompletionList,
    CompletionItem,
    Position,
    CompletionItemTag,
    MarkupContent,
    Hover,
    MarkupKind,
    HoverParams,
    FoldingRangeParams,
    FoldingRange,
    FoldingRangeKind,
    SemanticTokensParams,
    SemanticTokens,
)

try:
    from pygls.server import LanguageServer
    from pygls.workspace import TextDocument
except ImportError:
    pass


_CONTAINS_SPACE_OR_COLON = re.compile(r"[\s:]")


def _at_cursor(
    doc: "TextDocument",
    lines: List[str],
    client_position: Position,
) -> Tuple[Position, Optional[str], str, bool, int, Set[str]]:
    paragraph_no = -1
    paragraph_started = False
    seen_fields: Set[str] = set()
    last_field_seen: Optional[str] = None
    current_field: Optional[str] = None
    server_position = doc.position_codec.position_from_client_units(
        lines,
        client_position,
    )
    position_line_no = server_position.line

    line_at_position = lines[position_line_no]
    line_start = ""
    if server_position.character:
        line_start = line_at_position[0 : server_position.character]

    for line_no, line in enumerate(lines):
        if not line or line.isspace():
            if line_no == position_line_no:
                current_field = last_field_seen
                continue
            last_field_seen = None
            if line_no > position_line_no:
                break
            paragraph_started = False
        elif line and line[0] == "#":
            continue
        elif line and not line[0].isspace() and ":" in line:
            if not paragraph_started:
                paragraph_started = True
                seen_fields = set()
                paragraph_no += 1
            key, _ = line.split(":", 1)
            key_lc = key.lower()
            last_field_seen = key_lc
            if line_no == position_line_no:
                current_field = key_lc
            seen_fields.add(key_lc)

    in_value = bool(_CONTAINS_SPACE_OR_COLON.search(line_start))
    current_word = doc.word_at_position(client_position)
    if current_field is not None:
        current_field = normalize_dctrl_field_name(current_field)
    return (
        server_position,
        current_field,
        current_word,
        in_value,
        paragraph_no,
        seen_fields,
    )


def deb822_completer(
    ls: "DebputyLanguageServer",
    params: CompletionParams,
    file_metadata: Deb822FileMetadata[Any],
) -> Optional[Union[CompletionList, Sequence[CompletionItem]]]:
    doc = ls.workspace.get_text_document(params.text_document.uri)
    lines = doc.lines

    _a, current_field, _b, in_value, paragraph_no, seen_fields = _at_cursor(
        doc,
        lines,
        params.position,
    )

    stanza_metadata = file_metadata.guess_stanza_classification_by_idx(paragraph_no)

    if in_value:
        _info(f"Completion for field value {current_field}")
        if current_field is None:
            return None
        known_field = stanza_metadata.get(current_field)
        if known_field is None:
            return None
        items = _complete_field_value(known_field)
    else:
        _info("Completing field name")
        items = _complete_field_name(
            ls,
            stanza_metadata,
            seen_fields,
        )

    _info(f"Completion candidates: {items}")

    return items


def deb822_hover(
    ls: "DebputyLanguageServer",
    params: HoverParams,
    file_metadata: Deb822FileMetadata[S],
    *,
    custom_handler: Optional[
        Callable[
            [
                Position,
                Optional[str],
                str,
                Optional[F],
                bool,
                "TextDocument",
                List[str],
            ],
            Optional[Hover],
        ]
    ] = None,
) -> Optional[Hover]:
    doc = ls.workspace.get_text_document(params.text_document.uri)
    lines = doc.lines
    server_pos, current_field, word_at_position, in_value, paragraph_no, _ = _at_cursor(
        doc, lines, params.position
    )
    stanza_metadata = file_metadata.guess_stanza_classification_by_idx(paragraph_no)

    known_field = (
        stanza_metadata.get(current_field) if current_field is not None else None
    )
    hover_text = None
    if custom_handler is not None:
        res = custom_handler(
            server_pos,
            current_field,
            word_at_position,
            known_field,
            in_value,
            doc,
            lines,
        )
        if isinstance(res, Hover):
            return res
        hover_text = res

    if hover_text is None:
        if current_field is None:
            _info("No hover information as we cannot determine which field it is for")
            return None

        if known_field is None:
            return None
        if in_value:
            if not known_field.known_values:
                return None
            keyword = known_field.known_values.get(word_at_position)
            if keyword is None:
                return None
            hover_text = keyword.hover_text
            if hover_text is not None:
                hover_text = f"# Value `{keyword.value}` (Field: {known_field.name})\n\n{hover_text}"
        else:
            hover_text = known_field.hover_text
            if hover_text is None:
                hover_text = f"The field {current_field} had no documentation."
            hover_text = f"# {known_field.name}\n\n{hover_text}"

    if hover_text is None:
        return None
    return Hover(
        contents=MarkupContent(
            kind=ls.hover_markup_format(MarkupKind.Markdown, MarkupKind.PlainText),
            value=hover_text,
        )
    )


def deb822_token_iter(
    tokens: Iterable[Deb822Token],
) -> Iterator[Tuple[Deb822Token, int, int, int, int]]:
    line_no = 0
    line_offset = 0

    for token in tokens:
        start_line = line_no
        start_line_offset = line_offset

        newlines = token.text.count("\n")
        line_no += newlines
        text_len = len(token.text)
        if newlines:
            if token.text.endswith("\n"):
                line_offset = 0
            else:
                # -2, one to remove the "\n" and one to get 0-offset
                line_offset = text_len - token.text.rindex("\n") - 2
        else:
            line_offset += text_len

        yield token, start_line, start_line_offset, line_no, line_offset


def deb822_folding_ranges(
    ls: "DebputyLanguageServer",
    params: FoldingRangeParams,
    # Unused for now: might be relevant for supporting folding for some fields
    _file_metadata: Deb822FileMetadata[Any],
) -> Optional[Sequence[FoldingRange]]:
    doc = ls.workspace.get_text_document(params.text_document.uri)
    comment_start = -1
    folding_ranges = []
    for (
        token,
        start_line,
        start_offset,
        end_line,
        end_offset,
    ) in deb822_token_iter(tokenize_deb822_file(doc.lines)):
        if token.is_comment:
            if comment_start < 0:
                comment_start = start_line
        elif comment_start > -1:
            comment_start = -1
            folding_range = FoldingRange(
                comment_start,
                end_line,
                kind=FoldingRangeKind.Comment,
            )

            folding_ranges.append(folding_range)

    return folding_ranges


@dataclasses.dataclass(slots=True)
class SemanticTokenState:
    ls: "DebputyLanguageServer"
    file_metadata: Deb822FileMetadata[Any]
    doc: "TextDocument"
    lines: List[str]
    tokens: List[int]
    keyword_token_code: int
    known_value_token_code: int
    comment_token_code: int
    _previous_line: int = 0
    _previous_col: int = 0

    def emit_token(
        self,
        start_pos: Position,
        len_client_units: int,
        token_code: int,
        *,
        token_modifiers: int = 0,
    ) -> None:
        line_delta = start_pos.line - self._previous_line
        self._previous_line = start_pos.line
        previous_col = self._previous_col

        if line_delta:
            previous_col = 0

        column_delta = start_pos.character - previous_col
        self._previous_col = start_pos.character

        tokens = self.tokens
        tokens.append(line_delta)  # Line delta
        tokens.append(column_delta)  # Token column delta
        tokens.append(len_client_units)  # Token length
        tokens.append(token_code)
        tokens.append(token_modifiers)


def _deb822_paragraph_semantic_tokens_full(
    sem_token_state: SemanticTokenState,
    stanza: Deb822ParagraphElement,
    stanza_idx: int,
) -> None:
    doc = sem_token_state.doc
    keyword_token_code = sem_token_state.keyword_token_code
    known_value_token_code = sem_token_state.known_value_token_code
    comment_token_code = sem_token_state.comment_token_code

    stanza_position = stanza.position_in_file()
    stanza_metadata = sem_token_state.file_metadata.classify_stanza(
        stanza,
        stanza_idx=stanza_idx,
    )
    for kvpair in stanza.iter_parts_of_type(Deb822KeyValuePairElement):
        field_start = kvpair.key_position_in_stanza().relative_to(stanza_position)
        comment = kvpair.comment_element
        if comment:
            comment_start_line = field_start.line_position - len(comment)
            for comment_line_no, comment_token in enumerate(
                comment.iter_parts(),
                start=comment_start_line,
            ):
                assert comment_token.is_comment
                assert isinstance(comment_token, Deb822Token)
                sem_token_state.emit_token(
                    Position(comment_line_no, 0),
                    len(comment_token.text.rstrip()),
                    comment_token_code,
                )
        field_size = doc.position_codec.client_num_units(kvpair.field_name)

        sem_token_state.emit_token(
            te_position_to_lsp(field_start),
            field_size,
            keyword_token_code,
        )

        known_field: Optional[Deb822KnownField] = stanza_metadata.get(kvpair.field_name)
        if known_field is not None:
            if known_field.spellcheck_value:
                continue
            known_values: Container[str] = known_field.known_values or frozenset()
            interpretation = known_field.field_value_class.interpreter()
        else:
            known_values = frozenset()
            interpretation = None

        value_element_pos = kvpair.value_position_in_stanza().relative_to(
            stanza_position
        )
        if interpretation is None:
            # TODO: Emit tokens for value comments of unknown fields.
            continue
        else:
            parts = kvpair.interpret_as(interpretation).iter_parts()
        for te in parts:
            if te.is_whitespace:
                continue
            if te.is_separator:
                continue
            value_range_in_parent_te = te.range_in_parent()
            value_range_te = value_range_in_parent_te.relative_to(value_element_pos)
            value = te.convert_to_text()
            if te.is_comment:
                token_type = comment_token_code
                value = value.rstrip()
            elif value in known_values:
                token_type = known_value_token_code
            else:
                continue
            value_len = doc.position_codec.client_num_units(value)

            sem_token_state.emit_token(
                te_position_to_lsp(value_range_te.start_pos),
                value_len,
                token_type,
            )


def deb822_semantic_tokens_full(
    ls: "DebputyLanguageServer",
    request: SemanticTokensParams,
    file_metadata: Deb822FileMetadata[Any],
) -> Optional[SemanticTokens]:
    doc = ls.workspace.get_text_document(request.text_document.uri)
    position_codec = doc.position_codec
    lines = doc.lines
    deb822_file = parse_deb822_file(
        lines,
        accept_files_with_duplicated_fields=True,
        accept_files_with_error_tokens=True,
    )
    tokens: List[int] = []
    comment_token_code = SEMANTIC_TOKEN_TYPES_IDS["comment"]
    sem_token_state = SemanticTokenState(
        ls,
        file_metadata,
        doc,
        lines,
        tokens,
        SEMANTIC_TOKEN_TYPES_IDS["keyword"],
        SEMANTIC_TOKEN_TYPES_IDS["enumMember"],
        comment_token_code,
    )

    stanza_idx = 0

    for part in deb822_file.iter_parts():
        if part.is_comment:
            pos = part.position_in_file()
            sem_token_state.emit_token(
                te_position_to_lsp(pos),
                # Avoid trailing newline
                position_codec.client_num_units(part.convert_to_text().rstrip()),
                comment_token_code,
            )
        elif isinstance(part, Deb822ParagraphElement):
            _deb822_paragraph_semantic_tokens_full(
                sem_token_state,
                part,
                stanza_idx,
            )
            stanza_idx += 1
    if not tokens:
        return None
    return SemanticTokens(tokens)


def _should_complete_field_with_value(cand: Deb822KnownField) -> bool:
    return cand.known_values is not None and (
        len(cand.known_values) == 1
        or (
            len(cand.known_values) == 2
            and cand.warn_if_default
            and cand.default_value is not None
        )
    )


def _complete_field_name(
    ls: "DebputyLanguageServer",
    fields: StanzaMetadata[Any],
    seen_fields: Container[str],
) -> Optional[Union[CompletionList, Sequence[CompletionItem]]]:
    items = []
    markdown_kind = ls.completion_item_document_markup(
        MarkupKind.Markdown, MarkupKind.PlainText
    )
    for cand_key, cand in fields.items():
        if cand_key.lower() in seen_fields:
            continue
        name = cand.name
        complete_as = name + ": "
        if _should_complete_field_with_value(cand):
            value = next(iter(v for v in cand.known_values if v != cand.default_value))
            complete_as += value
        tags = []
        is_deprecated = False
        if cand.replaced_by or cand.deprecated_with_no_replacement:
            is_deprecated = True
            tags.append(CompletionItemTag.Deprecated)

        doc = cand.hover_text
        if doc:
            doc = MarkupContent(
                value=doc,
                kind=markdown_kind,
            )
        else:
            doc = None

        items.append(
            CompletionItem(
                name,
                insert_text=complete_as,
                deprecated=is_deprecated,
                tags=tags,
                detail=cand.synopsis_doc,
                documentation=doc,
            )
        )
    return items


def _complete_field_value(
    field: Deb822KnownField,
) -> Optional[Union[CompletionList, Sequence[CompletionItem]]]:
    if field.known_values is None:
        return None
    return [CompletionItem(v) for v in field.known_values]
