import textwrap
from functools import lru_cache
from typing import (
    Optional,
    List,
    Any,
    Tuple,
    Union,
    Iterable,
    Sequence,
    Literal,
    get_args,
    get_origin,
    TYPE_CHECKING,
)

from debputy.highlevel_manifest import MANIFEST_YAML
from debputy.linting.lint_util import LintState
from debputy.lsp.lsp_features import (
    lint_diagnostics,
    lsp_standard_handler,
    lsp_hover,
    lsp_completer,
    LanguageDispatchRule,
    SecondaryLanguage,
)
from debputy.lsp.lsp_generic_yaml import (
    resolve_hover_text,
    as_hover_doc,
    is_before,
    word_range_at_position,
    insert_complete_marker_snippet,
    YAML_COMPLETION_HINT_KEY,
    YAML_COMPLETION_HINT_VALUE,
    yaml_flag_unknown_key,
)
from debputy.lsp.text_util import (
    LintCapablePositionCodec,
)
from debputy.manifest_parser.base_types import (
    DebputyParsedContent,
)
from debputy.manifest_parser.declarative_parser import (
    AttributeDescription,
    ParserGenerator,
    DeclarativeNonMappingInputParser,
)
from debputy.manifest_parser.declarative_parser import DeclarativeMappingInputParser
from debputy.manifest_parser.parser_data import ParserContextData
from debputy.manifest_parser.tagging_types import DebputyDispatchableType
from debputy.manifest_parser.util import AttributePath
from debputy.plugin.api.impl import plugin_metadata_for_debputys_own_plugin
from debputy.plugin.api.impl_types import (
    DeclarativeInputParser,
    DispatchingParserBase,
    DebputyPluginMetadata,
    ListWrappedDeclarativeInputParser,
    InPackageContextParser,
    DeclarativeValuelessKeywordInputParser,
    DispatchingObjectParser,
)
from debputy.plugin.api.spec import ParserDocumentation, reference_documentation
from debputy.util import _info
from debputy.yaml.compat import (
    Node,
    CommentedMap,
    LineCol,
    CommentedSeq,
    CommentedBase,
    MarkedYAMLError,
    YAMLError,
)

try:
    from debputy.lsp.debputy_ls import DebputyLanguageServer
except ImportError:
    pass

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


_DISPATCH_RULE = LanguageDispatchRule.new_rule(
    "debian/upstream/metadata",
    "debian/upstream/metadata",
    [SecondaryLanguage("yaml", filename_based_lookup=True)],
)


lsp_standard_handler(_DISPATCH_RULE, types.TEXT_DOCUMENT_CODE_ACTION)
lsp_standard_handler(_DISPATCH_RULE, types.TEXT_DOCUMENT_WILL_SAVE_WAIT_UNTIL)


class StrDebputyParsedContent(DebputyParsedContent):
    content: str


def _parser_handler(
    _key: str,
    value: Any,
    _attr_path: AttributePath,
    _context: Optional["ParserContextData"],
) -> Any:
    return value


def add_keyword(
    pg: ParserGenerator,
    root_parser: DispatchingParserBase[Any],
    plugin_metadata: DebputyPluginMetadata,
    keyword: str,
    *,
    inline_reference_documentation: Optional[ParserDocumentation] = None,
) -> None:
    parser = pg.generate_parser(
        StrDebputyParsedContent,
        source_content=str,
        inline_reference_documentation=inline_reference_documentation,
    )
    root_parser.register_parser(
        keyword,
        parser,
        _parser_handler,
        plugin_metadata,
    )


@lru_cache
def root_object_parser() -> DispatchingObjectParser:
    plugin_metadata = plugin_metadata_for_debputys_own_plugin()
    pg = ParserGenerator()
    pg.add_object_parser(
        "<ROOT>",
        unknown_keys_diagnostic_severity=types.DiagnosticSeverity.Warning,
    )
    root_parser = pg.dispatchable_object_parsers["<ROOT>"]
    add_keyword(
        pg,
        root_parser,
        plugin_metadata,
        "Archive",
        inline_reference_documentation=reference_documentation(
            title="Archive (`Archive`)",
            description=textwrap.dedent(
                """\
                The name of the large archive that the upstream work is part of, like CPAN.
            """
            ),
        ),
    )
    add_keyword(
        pg,
        root_parser,
        plugin_metadata,
        "ASCL-Id",
        inline_reference_documentation=reference_documentation(
            title="ASCL Identifier (`ASCL-Id`)",
            description=textwrap.dedent(
                """\
                Identification code in the http://ascl.net
            """
            ),
        ),
    )
    add_keyword(
        pg,
        root_parser,
        plugin_metadata,
        "Bug-Database",
        inline_reference_documentation=reference_documentation(
            title="Bug database or tracker for the project (`Bug-Database`)",
            description=textwrap.dedent(
                """\
                A URL to the list of known bugs for the project.
            """
            ),
        ),
    )
    add_keyword(
        pg,
        root_parser,
        plugin_metadata,
        "Bug-Submit",
        inline_reference_documentation=reference_documentation(
            title="Bug submission URL for the project (`Bug-Submit`)",
            description=textwrap.dedent(
                """\
                A URL that is the place where new bug reports should be sent.
            """
            ),
        ),
    )
    add_keyword(
        pg,
        root_parser,
        plugin_metadata,
        "Cite-As",
        inline_reference_documentation=reference_documentation(
            title="Cite-As (`Cite-As`)",
            description=textwrap.dedent(
                """\
                The way the authors want their software be cited in publications.

                The value is a string which might contain a link in valid HTML syntax.
            """
            ),
        ),
    )
    add_keyword(
        pg,
        root_parser,
        plugin_metadata,
        "Changelog",
        inline_reference_documentation=reference_documentation(
            title="Changelog (`Changelog`)",
            description=textwrap.dedent(
                """\
                URL to the upstream changelog.
            """
            ),
        ),
    )
    add_keyword(
        pg,
        root_parser,
        plugin_metadata,
        "CPE",
        inline_reference_documentation=reference_documentation(
            title="CPE (`CPE`)",
            description=textwrap.dedent(
                """\
                One or more space separated http://cpe.mitre.org/ values useful to look up relevant CVEs
                in the https://nvd.nist.gov/home.cfm and other CVE sources.

                See `CPEtagPackagesDep` for information on how this information can be used.
                **Example**: `cpe:/a:ethereal_group:ethereal`
            """
            ),
        ),
    )
    add_keyword(
        pg,
        root_parser,
        plugin_metadata,
        "Documentation",
        inline_reference_documentation=reference_documentation(
            title="Documentation (`Documentation`)",
            description=textwrap.dedent(
                """\
                A URL to online documentation.
            """
            ),
        ),
    )
    add_keyword(
        pg,
        root_parser,
        plugin_metadata,
        "Donation",
        inline_reference_documentation=reference_documentation(
            title="Donation (`Donation`)",
            description=textwrap.dedent(
                """\
                A URL to a donation form (or instructions).
            """
            ),
        ),
    )
    add_keyword(
        pg,
        root_parser,
        plugin_metadata,
        "FAQ",
        inline_reference_documentation=reference_documentation(
            title="FAQ (`FAQ`)",
            description=textwrap.dedent(
                """\
                A URL to the online FAQ.
            """
            ),
        ),
    )
    add_keyword(
        pg,
        root_parser,
        plugin_metadata,
        "Funding",
        inline_reference_documentation=reference_documentation(
            title="Funding (`Funding`)",
            description=textwrap.dedent(
                """\
                One or more sources of funding which have supported this project (e.g. NSF OCI-12345).
            """
            ),
        ),
    )
    add_keyword(
        pg,
        root_parser,
        plugin_metadata,
        "Gallery",
        inline_reference_documentation=reference_documentation(
            title="Gallery (`Gallery`)",
            description=textwrap.dedent(
                """\
                A URL to a gallery of pictures made with the program (not screenshots).
            """
            ),
        ),
    )
    add_keyword(
        pg,
        root_parser,
        plugin_metadata,
        "Other-References",
        inline_reference_documentation=reference_documentation(
            title="Other-References (`Other-References`)",
            description=textwrap.dedent(
                """\
                A URL to a upstream page containing more references.
            """
            ),
        ),
    )
    add_keyword(
        pg,
        root_parser,
        plugin_metadata,
        "Reference",
        inline_reference_documentation=reference_documentation(
            title="Reference (`Reference`)",
            # FIXME: Add the fields below as a nested subobject or list of such objects
            description=textwrap.dedent(
                """\
                One or more bibliographic references, represented as a mapping or sequence of mappings containing
                the one or more of the following keys.
 
                The values for the keys are always scalars, and the keys that correspond to standard BibTeX
                entries must provide the same content.
            """
            ),
        ),
    )

    # Reference:: One or more bibliographic references, represented as a mapping or sequence of mappings containing the one or more of the following keys. The values for the keys are always scalars, and the keys that correspond to standard BibTeX entries must provide the same content.
    #
    # Author:: Author list in BibTeX friendly syntax (separating multiple authors by the keyword "and" and using as few as possible abbreviations in the names, as proposed in http://nwalsh.com/tex/texhelp/bibtx-23.html).
    #
    # Booktitle:: Title of the book the article is published in
    #
    # DOI:: This is the digital object identifier of the academic publication describing the packaged work.
    #
    # Editor:: Editor of the book the article is published in
    #
    # Eprint:: Hyperlink to the PDF file of the article.
    #
    # ISBN:: International Standard Book Number of the book if the article is part of the book or the reference is a book
    #
    # ISSN:: International Standard Serial Number of the periodical publication if the article is part of a series
    #
    # Journal:: Abbreviated journal name [To be discussed: which standard to recommend ?].
    #
    # Number:: Issue number.
    #
    # Pages:: Article page number(s). [To be discussed] Page number separator must be a single ASCII hyphen. What do we do with condensed notations like 401-10 ?
    #
    # PMID:: ID number in the https://www.ncbi.nlm.nih.gov/pubmed/ database.
    #
    # Publisher:: Publisher of the book containing the article
    #
    # Title:: Article title.
    #
    # Type:: A http://www.bibtex.org/Format indicating what is cited. Typical values are {{{article}}}, {{{book}}}, or {{{inproceedings}}}. [To be discussed]. In case this field is not present, {{{article}}} is assumed.
    #
    # URL:: Hyperlink to the abstract of the article. This should not point to the full version because this is specified by Eprint. Please also do not drop links to pubmed here because this would be redundant to PMID.
    #
    # Volume:: Journal volume.
    #
    # Year:: Year of publication
    #

    add_keyword(
        pg,
        root_parser,
        plugin_metadata,
        "Registration",
        inline_reference_documentation=reference_documentation(
            title="Registration (`Registration`)",
            description=textwrap.dedent(
                """\
                A URL to a registration form (or instructions). This could be registration of bug reporting
                accounts, registration for counting/contacting users etc.
            """
            ),
        ),
    )
    add_keyword(
        pg,
        root_parser,
        plugin_metadata,
        "Registry",
        # FIXME: Add List of `Name`, `Entry` objects
        inline_reference_documentation=reference_documentation(
            title="Registry (`Registry`)",
            description=textwrap.dedent(
                """\
                    This field shall point to external catalogs/registries of software.
 
                    The field features an array of "Name (of registry) - Entry (ID of software in that catalog)" pairs.
                    The names and entries shall only be names, not complete URIs, to avoid any bias on mirrors etc.
                    Example:
                    ```yaml
                       Registry:
                         - Name: bio.tools
                           Entry: clustalw
                         - Name: OMICtools
                           Entry: OMICS_02562
                         - Name: SciCrunch
                           Entry: SCR_002909
                    ```
            """
            ),
        ),
    )

    add_keyword(
        pg,
        root_parser,
        plugin_metadata,
        "Repository",
        inline_reference_documentation=reference_documentation(
            title="Repository (`Repository`)",
            description=textwrap.dedent(
                """\
                    URL to a repository containing the upstream sources.
            """
            ),
        ),
    )

    add_keyword(
        pg,
        root_parser,
        plugin_metadata,
        "Repository-Browse",
        inline_reference_documentation=reference_documentation(
            title="Repository-Browse (`Repository-Browse`)",
            description=textwrap.dedent(
                """\
                     A URL to browse the repository containing the upstream sources.
            """
            ),
        ),
    )
    add_keyword(
        pg,
        root_parser,
        plugin_metadata,
        "Screenshots",
        inline_reference_documentation=reference_documentation(
            title="Screenshots (`Screenshots`)",
            description=textwrap.dedent(
                """\
                     One or more URLs to upstream pages containing screenshots (not <https://screenshots.debian.net>),
                     represented by a scalar or a sequence of scalars.
            """
            ),
        ),
    )
    add_keyword(
        pg,
        root_parser,
        plugin_metadata,
        "Security-Contact",
        inline_reference_documentation=reference_documentation(
            title="Security-Contact (`Security-Contact`)",
            description=textwrap.dedent(
                """\
                      Which person, mailing list, forum, etc. to send security-related messages in the first place.
            """
            ),
        ),
    )
    add_keyword(
        pg,
        root_parser,
        plugin_metadata,
        "Webservice",
        inline_reference_documentation=reference_documentation(
            title="Webservice (`Webservice`)",
            description=textwrap.dedent(
                """\
                      URL to a web page where the packaged program can also be used.
            """
            ),
        ),
    )
    return root_parser


@lint_diagnostics(_DISPATCH_RULE)
def _lint_debian_upstream_metadata(
    lint_state: LintState,
) -> Optional[List[types.Diagnostic]]:
    lines = lint_state.lines
    position_codec = lint_state.position_codec
    diagnostics: List[types.Diagnostic] = []

    try:
        content = MANIFEST_YAML.load("".join(lines))
    except MarkedYAMLError as e:
        if e.context_mark:
            line = e.context_mark.line
            column = e.context_mark.column + 1
        else:
            line = e.problem_mark.line
            column = e.problem_mark.column + 1
        error_range = position_codec.range_to_client_units(
            lines,
            word_range_at_position(
                lines,
                line,
                column,
            ),
        )
        diagnostics.append(
            types.Diagnostic(
                error_range,
                f"YAML parse error: {e}",
                types.DiagnosticSeverity.Error,
            ),
        )
    except YAMLError as e:
        error_range = position_codec.range_to_client_units(
            lines,
            types.Range(
                types.Position(0, 0),
                types.Position(0, len(lines[0])),
            ),
        )
        diagnostics.append(
            types.Diagnostic(
                error_range,
                f"Unknown YAML parse error: {e} [{e!r}]",
                types.DiagnosticSeverity.Error,
            ),
        )
    else:
        feature_set = lint_state.plugin_feature_set
        pg = feature_set.manifest_parser_generator
        root_parser = root_object_parser()
        diagnostics.extend(
            _lint_content(
                lint_state,
                pg,
                root_parser,
                content,
            )
        )
    return diagnostics


def _conflicting_key(
    uri: str,
    key_a: str,
    key_b: str,
    key_a_line: int,
    key_a_col: int,
    key_b_line: int,
    key_b_col: int,
    lines: List[str],
    position_codec: LintCapablePositionCodec,
) -> Iterable[types.Diagnostic]:
    key_a_range = position_codec.range_to_client_units(
        lines,
        types.Range(
            types.Position(
                key_a_line,
                key_a_col,
            ),
            types.Position(
                key_a_line,
                key_a_col + len(key_a),
            ),
        ),
    )
    key_b_range = position_codec.range_to_client_units(
        lines,
        types.Range(
            types.Position(
                key_b_line,
                key_b_col,
            ),
            types.Position(
                key_b_line,
                key_b_col + len(key_b),
            ),
        ),
    )
    yield types.Diagnostic(
        key_a_range,
        f'The "{key_a}" cannot be used with "{key_b}".',
        types.DiagnosticSeverity.Error,
        source="debputy",
        related_information=[
            types.DiagnosticRelatedInformation(
                location=types.Location(
                    uri,
                    key_b_range,
                ),
                message=f'The attribute "{key_b}" is used here.',
            )
        ],
    )

    yield types.Diagnostic(
        key_b_range,
        f'The "{key_b}" cannot be used with "{key_a}".',
        types.DiagnosticSeverity.Error,
        source="debputy",
        related_information=[
            types.DiagnosticRelatedInformation(
                location=types.Location(
                    uri,
                    key_a_range,
                ),
                message=f'The attribute "{key_a}" is used here.',
            )
        ],
    )


def _lint_attr_value(
    lint_state: LintState,
    attr: AttributeDescription,
    pg: ParserGenerator,
    value: Any,
) -> Iterable["types.Diagnostic"]:
    attr_type = attr.attribute_type
    orig = get_origin(attr_type)
    valid_values: Sequence[Any] = tuple()
    if orig == Literal:
        valid_values = get_args(attr.attribute_type)
    elif orig == bool or attr.attribute_type == bool:
        valid_values = ("true", "false")
    elif isinstance(attr_type, type) and issubclass(attr_type, DebputyDispatchableType):
        parser = pg.dispatch_parser_table_for(attr_type)
        yield from _lint_content(
            lint_state,
            pg,
            parser,
            value,
        )
        return

    if value in valid_values:
        return
    # TODO: Emit diagnostic for broken values
    return


def _lint_declarative_mapping_input_parser(
    lint_state: LintState,
    pg: ParserGenerator,
    parser: DeclarativeMappingInputParser,
    content: Any,
) -> Iterable["types.Diagnostic"]:
    if not isinstance(content, CommentedMap):
        return
    lc = content.lc
    for key, value in content.items():
        attr = parser.manifest_attributes.get(key)
        line, col = lc.key(key)
        if attr is None:
            diag, corrected_key = yaml_flag_unknown_key(
                key,
                parser.manifest_attributes,
                line,
                col,
                lint_state.lines,
                lint_state.position_codec,
            )
            yield diag
            if corrected_key:
                key = corrected_key
                attr = parser.manifest_attributes.get(corrected_key)
        if attr is None:
            continue

        yield from _lint_attr_value(
            lint_state,
            attr,
            pg,
            value,
        )

        for forbidden_key in attr.conflicting_attributes:
            if forbidden_key in content:
                con_line, con_col = lc.key(forbidden_key)
                yield from _conflicting_key(
                    lint_state.doc_uri,
                    key,
                    forbidden_key,
                    line,
                    col,
                    con_line,
                    con_col,
                    lint_state.lines,
                    lint_state.position_codec,
                )
    for mx in parser.mutually_exclusive_attributes:
        matches = content.keys() & mx
        if len(matches) < 2:
            continue
        key, *others = list(matches)
        line, col = lc.key(key)
        for other in others:
            con_line, con_col = lc.key(other)
            yield from _conflicting_key(
                lint_state.doc_uri,
                key,
                other,
                line,
                col,
                con_line,
                con_col,
                lint_state.lines,
                lint_state.position_codec,
            )


def _lint_content(
    lint_state: LintState,
    pg: ParserGenerator,
    parser: DeclarativeInputParser[Any],
    content: Any,
) -> Iterable["types.Diagnostic"]:
    if isinstance(parser, DispatchingParserBase):
        if not isinstance(content, CommentedMap):
            return
        lc = content.lc
        for key, value in content.items():
            is_known = parser.is_known_keyword(key)
            if not is_known:
                line, col = lc.key(key)
                diag, corrected_key = yaml_flag_unknown_key(
                    key,
                    parser.registered_keywords(),
                    line,
                    col,
                    lint_state.lines,
                    lint_state.position_codec,
                    unknown_keys_diagnostic_severity=parser.unknown_keys_diagnostic_severity,
                )
                if diag is not None:
                    yield diag
                if corrected_key is not None:
                    key = corrected_key
                    is_known = True

            if is_known:
                subparser = parser.parser_for(key)
                assert subparser is not None
                yield from _lint_content(
                    lint_state,
                    pg,
                    subparser.parser,
                    value,
                )
    elif isinstance(parser, ListWrappedDeclarativeInputParser):
        if not isinstance(content, CommentedSeq):
            return
        subparser = parser.delegate
        for value in content:
            yield from _lint_content(lint_state, pg, subparser, value)
    elif isinstance(parser, InPackageContextParser):
        if not isinstance(content, CommentedMap):
            return
        print(lint_state)
        known_packages = lint_state.binary_packages
        lc = content.lc
        for k, v in content.items():
            if "{{" not in k and known_packages is not None and k not in known_packages:
                line, col = lc.key(k)
                diag, _ = yaml_flag_unknown_key(
                    k,
                    known_packages,
                    line,
                    col,
                    lint_state.lines,
                    lint_state.position_codec,
                    message_format='Unknown package "{key}".',
                )
                yield diag
            yield from _lint_content(lint_state, pg, parser.delegate, v)
    elif isinstance(parser, DeclarativeMappingInputParser):
        yield from _lint_declarative_mapping_input_parser(
            lint_state,
            pg,
            parser,
            content,
        )


def _trace_cursor(
    content: Any,
    attribute_path: AttributePath,
    server_position: types.Position,
) -> Optional[Tuple[bool, AttributePath, Any, Any]]:
    matched_key: Optional[Union[str, int]] = None
    matched: Optional[Node] = None
    matched_was_key: bool = False

    if isinstance(content, CommentedMap):
        dict_lc: LineCol = content.lc
        for k, v in content.items():
            k_lc = dict_lc.key(k)
            if is_before(server_position, k_lc):
                break
            v_lc = dict_lc.value(k)
            if is_before(server_position, v_lc):
                # TODO: Handle ":" and "whitespace"
                matched = k
                matched_key = k
                matched_was_key = True
                break
            matched = v
            matched_key = k
    elif isinstance(content, CommentedSeq):
        list_lc: LineCol = content.lc
        for idx, value in enumerate(content):
            i_lc = list_lc.item(idx)
            if is_before(server_position, i_lc):
                break
            matched_key = idx
            matched = value

    if matched is not None:
        assert matched_key is not None
        sub_path = attribute_path[matched_key]
        if not matched_was_key and isinstance(matched, CommentedBase):
            return _trace_cursor(matched, sub_path, server_position)
        return matched_was_key, sub_path, matched, content
    return None


def resolve_keyword(
    current_parser: Union[DeclarativeInputParser[Any], DispatchingParserBase],
    current_plugin: DebputyPluginMetadata,
    segments: List[Union[str, int]],
    segment_idx: int,
    parser_generator: ParserGenerator,
    *,
    is_completion_attempt: bool = False,
) -> Optional[
    Tuple[
        Union[DeclarativeInputParser[Any], DispatchingParserBase],
        DebputyPluginMetadata,
        int,
    ]
]:
    if segment_idx >= len(segments):
        return current_parser, current_plugin, segment_idx
    current_segment = segments[segment_idx]
    if isinstance(current_parser, ListWrappedDeclarativeInputParser):
        if isinstance(current_segment, int):
            current_parser = current_parser.delegate
            segment_idx += 1
            if segment_idx >= len(segments):
                return current_parser, current_plugin, segment_idx
            current_segment = segments[segment_idx]

    if not isinstance(current_segment, str):
        return None

    if is_completion_attempt and current_segment.endswith(
        (YAML_COMPLETION_HINT_KEY, YAML_COMPLETION_HINT_VALUE)
    ):
        return current_parser, current_plugin, segment_idx

    if isinstance(current_parser, InPackageContextParser):
        return resolve_keyword(
            current_parser.delegate,
            current_plugin,
            segments,
            segment_idx + 1,
            parser_generator,
            is_completion_attempt=is_completion_attempt,
        )
    elif isinstance(current_parser, DispatchingParserBase):
        if not current_parser.is_known_keyword(current_segment):
            if is_completion_attempt:
                return current_parser, current_plugin, segment_idx
            return None
        subparser = current_parser.parser_for(current_segment)
        segment_idx += 1
        if segment_idx < len(segments):
            return resolve_keyword(
                subparser.parser,
                subparser.plugin_metadata,
                segments,
                segment_idx,
                parser_generator,
                is_completion_attempt=is_completion_attempt,
            )
        return subparser.parser, subparser.plugin_metadata, segment_idx
    elif isinstance(current_parser, DeclarativeMappingInputParser):
        attr = current_parser.manifest_attributes.get(current_segment)
        attr_type = attr.attribute_type if attr is not None else None
        if (
            attr_type is not None
            and isinstance(attr_type, type)
            and issubclass(attr_type, DebputyDispatchableType)
        ):
            subparser = parser_generator.dispatch_parser_table_for(attr_type)
            if subparser is not None and (
                is_completion_attempt or segment_idx + 1 < len(segments)
            ):
                return resolve_keyword(
                    subparser,
                    current_plugin,
                    segments,
                    segment_idx + 1,
                    parser_generator,
                    is_completion_attempt=is_completion_attempt,
                )
        return current_parser, current_plugin, segment_idx
    else:
        _info(f"Unknown parser: {current_parser.__class__}")
    return None


DEBPUTY_PLUGIN_METADATA = plugin_metadata_for_debputys_own_plugin()


@lsp_completer(_DISPATCH_RULE)
def debian_upstream_metadata_completer(
    ls: "DebputyLanguageServer",
    params: types.CompletionParams,
) -> Optional[Union[types.CompletionList, Sequence[types.CompletionItem]]]:
    doc = ls.workspace.get_text_document(params.text_document.uri)
    lines = doc.lines
    server_position = doc.position_codec.position_from_client_units(
        lines, params.position
    )
    added_key = insert_complete_marker_snippet(lines, server_position)
    attempts = 1 if added_key else 2
    content = None

    while attempts > 0:
        attempts -= 1
        try:
            content = MANIFEST_YAML.load("".join(lines))
            break
        except MarkedYAMLError as e:
            context_line = (
                e.context_mark.line if e.context_mark else e.problem_mark.line
            )
            if (
                e.problem_mark.line != server_position.line
                and context_line != server_position.line
            ):
                l_data = (
                    lines[e.problem_mark.line].rstrip()
                    if e.problem_mark.line < len(lines)
                    else "N/A (OOB)"
                )

                _info(f"Parse error on line: {e.problem_mark.line}: {l_data}")
                return None

            if attempts > 0:
                # Try to make it a key and see if that fixes the problem
                new_line = (
                    lines[server_position.line].rstrip() + YAML_COMPLETION_HINT_KEY
                )
                lines[server_position.line] = new_line
        except YAMLError:
            break
    if content is None:
        context = lines[server_position.line].replace("\n", "\\n")
        _info(f"Completion failed: parse error: Line in question: {context}")
        return None
    attribute_root_path = AttributePath.root_path(content)
    m = _trace_cursor(content, attribute_root_path, server_position)

    if m is None:
        _info("No match")
        return None
    matched_key, attr_path, matched, parent = m
    _info(f"Matched path: {matched} (path: {attr_path.path}) [{matched_key=}]")
    feature_set = ls.plugin_feature_set
    root_parser = root_object_parser()
    segments = list(attr_path.path_segments())
    km = resolve_keyword(
        root_parser,
        DEBPUTY_PLUGIN_METADATA,
        segments,
        0,
        feature_set.manifest_parser_generator,
        is_completion_attempt=True,
    )
    if km is None:
        return None
    parser, _, at_depth_idx = km
    _info(f"Match leaf parser {at_depth_idx} -- {parser.__class__}")
    items = []
    if at_depth_idx + 1 >= len(segments):
        if isinstance(parser, DispatchingParserBase):
            if matched_key:
                items = [
                    types.CompletionItem(f"{k}:")
                    for k in parser.registered_keywords()
                    if k not in parent
                    and not isinstance(
                        parser.parser_for(k).parser,
                        DeclarativeValuelessKeywordInputParser,
                    )
                ]
            else:
                items = [
                    types.CompletionItem(k)
                    for k in parser.registered_keywords()
                    if k not in parent
                    and isinstance(
                        parser.parser_for(k).parser,
                        DeclarativeValuelessKeywordInputParser,
                    )
                ]
        elif isinstance(parser, InPackageContextParser):
            binary_packages = ls.lint_state(doc).binary_packages
            if binary_packages is not None:
                items = [
                    types.CompletionItem(f"{p}:")
                    for p in binary_packages
                    if p not in parent
                ]
        elif isinstance(parser, DeclarativeMappingInputParser):
            if matched_key:
                _info("Match attributes")
                locked = set(parent)
                for mx in parser.mutually_exclusive_attributes:
                    if not mx.isdisjoint(parent.keys()):
                        locked.update(mx)
                for attr_name, attr in parser.manifest_attributes.items():
                    if not attr.conflicting_attributes.isdisjoint(parent.keys()):
                        locked.add(attr_name)
                        break
                items = [
                    types.CompletionItem(f"{k}:")
                    for k in parser.manifest_attributes
                    if k not in locked
                ]
            else:
                # Value
                key = segments[at_depth_idx] if len(segments) > at_depth_idx else None
                attr = parser.manifest_attributes.get(key)
                if attr is not None:
                    _info(f"Expand value / key: {key} -- {attr.attribute_type}")
                    items = _completion_from_attr(
                        attr,
                        feature_set.manifest_parser_generator,
                        matched,
                    )
                else:
                    _info(
                        f"Expand value / key: {key} -- !! {list(parser.manifest_attributes)}"
                    )
        elif isinstance(parser, DeclarativeNonMappingInputParser):
            attr = parser.alt_form_parser
            items = _completion_from_attr(
                attr,
                feature_set.manifest_parser_generator,
                matched,
            )
    return items


def _completion_from_attr(
    attr: AttributeDescription,
    pg: ParserGenerator,
    matched: Any,
) -> Optional[Union[types.CompletionList, Sequence[types.CompletionItem]]]:
    orig = get_origin(attr.attribute_type)
    valid_values: Sequence[Any] = tuple()
    if orig == Literal:
        valid_values = get_args(attr.attribute_type)
    elif orig == bool or attr.attribute_type == bool:
        valid_values = ("true", "false")
    elif isinstance(orig, type) and issubclass(orig, DebputyDispatchableType):
        parser = pg.dispatch_parser_table_for(orig)
        _info(f"M: {parser}")

    if matched in valid_values:
        _info(f"Already filled: {matched} is one of {valid_values}")
        return None
    if valid_values:
        return [types.CompletionItem(x) for x in valid_values]
    return None


@lsp_hover(_DISPATCH_RULE)
def debian_upstream_hover(
    ls: "DebputyLanguageServer",
    params: types.HoverParams,
) -> Optional[types.Hover]:
    doc = ls.workspace.get_text_document(params.text_document.uri)
    lines = doc.lines
    position_codec = doc.position_codec
    server_position = position_codec.position_from_client_units(lines, params.position)

    try:
        content = MANIFEST_YAML.load("".join(lines))
    except YAMLError:
        return None
    attribute_root_path = AttributePath.root_path(content)
    m = _trace_cursor(content, attribute_root_path, server_position)
    if m is None:
        _info("No match")
        return None
    matched_key, attr_path, matched, _ = m
    _info(f"Matched path: {matched} (path: {attr_path.path}) [{matched_key=}]")

    feature_set = ls.plugin_feature_set
    parser_generator = feature_set.manifest_parser_generator
    root_parser = root_object_parser()
    segments = list(attr_path.path_segments())
    km = resolve_keyword(
        root_parser,
        DEBPUTY_PLUGIN_METADATA,
        segments,
        0,
        parser_generator,
    )
    if km is None:
        _info("No keyword match")
        return None
    parser, plugin_metadata, at_depth_idx = km
    _info(f"Match leaf parser {at_depth_idx}/{len(segments)} -- {parser.__class__}")
    hover_doc_text = resolve_hover_text(
        feature_set,
        parser,
        plugin_metadata,
        segments,
        at_depth_idx,
        matched,
        matched_key,
    )
    return as_hover_doc(ls, hover_doc_text)
