import dataclasses
import importlib.resources
import re
from functools import lru_cache
from typing import (
    Union,
    Optional,
    TYPE_CHECKING,
    List,
    Self,
)
from collections.abc import Sequence, Mapping

from debputy.linting.lint_util import LintState, with_range_in_continuous_parts
from debputy.lsp.debputy_ls import DebputyLanguageServer
from debputy.lsp.lsp_debian_control_reference_data import (
    DebianWatch5FileMetadata,
    Deb822KnownField,
)

import debputy.lsp.data.deb822_data as deb822_ref_data_dir
from debputy.lsp.lsp_features import (
    lint_diagnostics,
    lsp_completer,
    lsp_hover,
    lsp_standard_handler,
    lsp_folding_ranges,
    lsp_semantic_tokens_full,
    lsp_will_save_wait_until,
    lsp_format_document,
    SecondaryLanguage,
    LanguageDispatchRule,
    lsp_cli_reformat_document,
)
from debputy.lsp.lsp_generic_deb822 import (
    deb822_completer,
    deb822_hover,
    deb822_folding_ranges,
    deb822_semantic_tokens_full,
    deb822_format_file,
    scan_for_syntax_errors_and_token_level_diagnostics,
)
from debputy.lsp.lsp_reference_keyword import LSP_DATA_DOMAIN
from debputy.lsp.ref_models.deb822_reference_parse_models import (
    GenericVariable,
    GENERIC_VARIABLE_REFERENCE_DATA_PARSER,
)
from debputy.lsp.text_util import markdown_urlify
from debian._deb822_repro import (
    Deb822ParagraphElement,
)
from debputy.lsprotocol.types import (
    CompletionItem,
    CompletionList,
    CompletionParams,
    HoverParams,
    Hover,
    TEXT_DOCUMENT_CODE_ACTION,
    SemanticTokens,
    SemanticTokensParams,
    FoldingRangeParams,
    FoldingRange,
    WillSaveTextDocumentParams,
    TextEdit,
    DocumentFormattingParams,
)
from debputy.manifest_parser.util import AttributePath
from debputy.yaml import MANIFEST_YAML

try:
    from debian._deb822_repro.locatable import (
        Position as TEPosition,
        Range as TERange,
    )

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


if TYPE_CHECKING:
    import lsprotocol.types as types
else:
    import debputy.lsprotocol.types as types


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

_DISPATCH_RULE = LanguageDispatchRule.new_rule(
    "debian/watch",
    None,
    "debian/watch",
    [
        # Presumably, emacs's name
        SecondaryLanguage("debian-watch"),
        # Presumably, vim's name
        SecondaryLanguage("debwatch"),
    ],
)

_DWATCH_FILE_METADATA = DebianWatch5FileMetadata()

lsp_standard_handler(_DISPATCH_RULE, TEXT_DOCUMENT_CODE_ACTION)


@dataclasses.dataclass(slots=True, frozen=True)
class VariableMetadata:
    name: str
    doc_uris: Sequence[str]
    synopsis: str
    description: str

    def render_metadata_fields(self) -> str:
        doc_uris = self.doc_uris
        parts = []
        if doc_uris:
            if len(doc_uris) == 1:
                parts.append(f"Documentation: {markdown_urlify(doc_uris[0])}")
            else:
                parts.append("Documentation:")
                parts.extend(f" - {markdown_urlify(uri)}" for uri in doc_uris)
        return "\n".join(parts)

    @classmethod
    def from_ref_data(cls, x: GenericVariable) -> "Self":
        doc = x.get("documentation", {})
        return cls(
            x["name"],
            doc.get("uris", []),
            doc.get("synopsis", ""),
            doc.get("long_description", ""),
        )


def dwatch_variables_metadata_basename() -> str:
    return "debian_watch_variables_data.yaml"


def _as_variables_metadata(
    args: list[VariableMetadata],
) -> Mapping[str, VariableMetadata]:
    r = {s.name: s for s in args}
    assert len(r) == len(args)
    return r


@lru_cache
def dwatch_variables_metadata() -> Mapping[str, VariableMetadata]:
    p = importlib.resources.files(deb822_ref_data_dir.__name__).joinpath(
        dwatch_variables_metadata_basename()
    )

    with p.open("r", encoding="utf-8") as fd:
        raw = MANIFEST_YAML.load(fd)

    attr_path = AttributePath.root_path(p)
    ref = GENERIC_VARIABLE_REFERENCE_DATA_PARSER.parse_input(raw, attr_path)
    return _as_variables_metadata(
        [VariableMetadata.from_ref_data(x) for x in ref["variables"]]
    )


def _custom_hover(
    ls: "DebputyLanguageServer",
    server_position: types.Position,
    _current_field: str | None,
    _word_at_position: str,
    _known_field: Deb822KnownField | None,
    in_value: bool,
    _doc: "TextDocument",
    lines: list[str],
) -> Hover | str | None:
    if not in_value:
        return None

    line_no = server_position.line
    line = lines[line_no]
    variable_search_ref = server_position.character
    variable = ""
    try:
        # Unlike ${} substvars where the start and end uses distinct characters, we cannot
        # know for certain whether we are at the start or end of a variable when we land
        # directly on a separator.
        try:
            variable_start = line.rindex("@", 0, variable_search_ref)
        except ValueError:
            if line[variable_search_ref] != "@":
                raise
            variable_start = variable_search_ref

        variable_end = line.index("@", variable_start + 1)
        if server_position.character <= variable_end:
            variable = line[variable_start : variable_end + 1]
    except (ValueError, IndexError):
        pass

    if variable != "" and variable != "@@":
        substvar_md = dwatch_variables_metadata().get(variable)

        if substvar_md is None:
            # In case of `@PACKAGE@-lin<CURSOR>ux-@ANY_VERSION@`
            return None
        doc = ls.translation(LSP_DATA_DOMAIN).pgettext(
            f"Variable:{substvar_md.name}",
            substvar_md.description,
        )
        md_fields = "\n" + substvar_md.render_metadata_fields()
        return f"# Variable `{variable}`\n\n{doc}{md_fields}"

    return None


@lsp_hover(_DISPATCH_RULE)
def _debian_watch_hover(
    ls: "DebputyLanguageServer",
    params: HoverParams,
) -> Hover | None:
    return deb822_hover(ls, params, _DWATCH_FILE_METADATA, custom_handler=_custom_hover)


@lsp_completer(_DISPATCH_RULE)
def _debian_watch_completions(
    ls: "DebputyLanguageServer",
    params: CompletionParams,
) -> CompletionList | Sequence[CompletionItem] | None:
    return deb822_completer(ls, params, _DWATCH_FILE_METADATA)


@lsp_folding_ranges(_DISPATCH_RULE)
def _debian_watch_folding_ranges(
    ls: "DebputyLanguageServer",
    params: FoldingRangeParams,
) -> Sequence[FoldingRange] | None:
    return deb822_folding_ranges(ls, params, _DWATCH_FILE_METADATA)


@lint_diagnostics(_DISPATCH_RULE)
async def _lint_debian_watch(lint_state: LintState) -> None:
    deb822_file = lint_state.parsed_deb822_file_content

    if not _DWATCH_FILE_METADATA.file_metadata_applies_to_file(deb822_file):
        return

    first_error = await scan_for_syntax_errors_and_token_level_diagnostics(
        deb822_file,
        lint_state,
    )
    header_stanza, source_stanza = _DWATCH_FILE_METADATA.stanza_types()
    stanza_no = 0

    async for stanza_range, stanza in lint_state.slow_iter(
        with_range_in_continuous_parts(deb822_file.iter_parts())
    ):
        if not isinstance(stanza, Deb822ParagraphElement):
            continue
        stanza_position = stanza_range.start_pos
        if stanza_position.line_position >= first_error:
            break
        stanza_no += 1
        is_source_stanza = stanza_no != 1
        if is_source_stanza:
            stanza_metadata = _DWATCH_FILE_METADATA.classify_stanza(
                stanza,
                stanza_no,
            )
            other_stanza_metadata = header_stanza
            other_stanza_name = "Header"
        elif "Version" in stanza:
            stanza_metadata = header_stanza
            other_stanza_metadata = source_stanza
            other_stanza_name = "Source"
        else:
            break

        await stanza_metadata.stanza_diagnostics(
            deb822_file,
            stanza,
            stanza_position,
            lint_state,
            confusable_with_stanza_name=other_stanza_name,
            confusable_with_stanza_metadata=other_stanza_metadata,
        )


@lsp_will_save_wait_until(_DISPATCH_RULE)
def _debian_watch_on_save_formatting(
    ls: "DebputyLanguageServer",
    params: WillSaveTextDocumentParams,
) -> Sequence[TextEdit] | None:
    doc = ls.workspace.get_text_document(params.text_document.uri)
    lint_state = ls.lint_state(doc)
    return deb822_format_file(lint_state, _DWATCH_FILE_METADATA)


@lsp_cli_reformat_document(_DISPATCH_RULE)
def _reformat_debian_watch(
    lint_state: LintState,
) -> Sequence[TextEdit] | None:
    return deb822_format_file(lint_state, _DWATCH_FILE_METADATA)


@lsp_format_document(_DISPATCH_RULE)
def _debian_watch_format_doc(
    ls: "DebputyLanguageServer",
    params: DocumentFormattingParams,
) -> Sequence[TextEdit] | None:
    doc = ls.workspace.get_text_document(params.text_document.uri)
    lint_state = ls.lint_state(doc)
    return deb822_format_file(lint_state, _DWATCH_FILE_METADATA)


@lsp_semantic_tokens_full(_DISPATCH_RULE)
async def _debian_watch_semantic_tokens_full(
    ls: "DebputyLanguageServer",
    request: SemanticTokensParams,
) -> SemanticTokens | None:
    return await deb822_semantic_tokens_full(
        ls,
        request,
        _DWATCH_FILE_METADATA,
    )
