from typing import Union, Any, Optional, List, Tuple, Iterable, TYPE_CHECKING

from debputy.lsp.diagnostics import DiagnosticData
from debputy.lsp.quickfixes import propose_correct_text_quick_fix
from debputy.lsp.text_util import LintCapablePositionCodec
from debputy.manifest_parser.declarative_parser import DeclarativeMappingInputParser
from debputy.manifest_parser.parser_doc import (
    render_rule,
    render_attribute_doc,
    doc_args_for_parser_doc,
)
from debputy.manifest_parser.tagging_types import DebputyDispatchableType
from debputy.plugin.api.feature_set import PluginProvidedFeatureSet
from debputy.plugin.api.impl_types import (
    DebputyPluginMetadata,
    DeclarativeInputParser,
    DispatchingParserBase,
)
from debputy.util import _info, _warn, detect_possible_typo

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

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


YAML_COMPLETION_HINT_KEY = "___COMPLETE:"
YAML_COMPLETION_HINT_VALUE = "___COMPLETE"


def resolve_hover_text_for_value(
    feature_set: PluginProvidedFeatureSet,
    parser: DeclarativeMappingInputParser,
    plugin_metadata: DebputyPluginMetadata,
    segment: Union[str, int],
    matched: Any,
) -> Optional[str]:

    hover_doc_text: Optional[str] = None
    attr = parser.manifest_attributes.get(segment)
    attr_type = attr.attribute_type if attr is not None else None
    if attr_type is None:
        _info(f"Matched value for {segment} -- No attr or type")
        return None
    if isinstance(attr_type, type) and issubclass(attr_type, DebputyDispatchableType):
        parser_generator = feature_set.manifest_parser_generator
        parser = parser_generator.dispatch_parser_table_for(attr_type)
        if parser is None or not isinstance(matched, str):
            _info(
                f"Unknown parser for {segment} or matched is not a str -- {attr_type} {type(matched)=}"
            )
            return None
        subparser = parser.parser_for(matched)
        if subparser is None:
            _info(f"Unknown parser for {matched} (subparser)")
            return None
        hover_doc_text = render_rule(
            matched,
            subparser.parser,
            plugin_metadata,
        )
    else:
        _info(f"Unknown value: {matched} -- {segment}")
    return hover_doc_text


def resolve_hover_text(
    feature_set: PluginProvidedFeatureSet,
    parser: Optional[Union[DeclarativeInputParser[Any], DispatchingParserBase]],
    plugin_metadata: DebputyPluginMetadata,
    segments: List[Union[str, int]],
    at_depth_idx: int,
    matched: Any,
    matched_key: bool,
) -> Optional[str]:
    hover_doc_text: Optional[str] = None
    if at_depth_idx == len(segments):
        segment = segments[at_depth_idx - 1]
        _info(f"Matched {segment} at ==, {matched_key=} ")
        hover_doc_text = render_rule(
            segment,
            parser,
            plugin_metadata,
            is_root_rule=False,
        )
    elif at_depth_idx + 1 == len(segments) and isinstance(
        parser, DeclarativeMappingInputParser
    ):
        segment = segments[at_depth_idx]
        _info(f"Matched {segment} at -1, {matched_key=} ")
        if isinstance(segment, str):
            if not matched_key:
                hover_doc_text = resolve_hover_text_for_value(
                    feature_set,
                    parser,
                    plugin_metadata,
                    segment,
                    matched,
                )
            if matched_key or hover_doc_text is None:
                rule_name = _guess_rule_name(segments, at_depth_idx)
                hover_doc_text = _render_param_doc(
                    rule_name,
                    parser,
                    plugin_metadata,
                    segment,
                )
    else:
        _info(f"No doc: {at_depth_idx=} {len(segments)=}")

    return hover_doc_text


def as_hover_doc(
    ls: "DebputyLanguageServer",
    hover_doc_text: Optional[str],
) -> Optional[types.Hover]:
    if hover_doc_text is None:
        return None
    return types.Hover(
        contents=types.MarkupContent(
            kind=ls.hover_markup_format(
                types.MarkupKind.Markdown,
                types.MarkupKind.PlainText,
            ),
            value=hover_doc_text,
        ),
    )


def _render_param_doc(
    rule_name: str,
    declarative_parser: DeclarativeMappingInputParser,
    plugin_metadata: DebputyPluginMetadata,
    attribute: str,
) -> Optional[str]:
    attr = declarative_parser.source_attributes.get(attribute)
    if attr is None:
        return None

    doc_args, parser_doc = doc_args_for_parser_doc(
        rule_name,
        declarative_parser,
        plugin_metadata,
    )
    rendered_docs = render_attribute_doc(
        declarative_parser,
        declarative_parser.source_attributes,
        declarative_parser.input_time_required_parameters,
        declarative_parser.at_least_one_of,
        parser_doc,
        doc_args,
        is_interactive=True,
        rule_name=rule_name,
    )

    for attributes, rendered_doc in rendered_docs:
        if attribute in attributes:
            full_doc = [
                f"# Attribute `{attribute}`",
                "",
            ]
            full_doc.extend(rendered_doc)

            return "\n".join(full_doc)
    return None


def _guess_rule_name(segments: List[Union[str, int]], idx: int) -> str:
    orig_idx = idx
    idx -= 1
    while idx >= 0:
        segment = segments[idx]
        if isinstance(segment, str):
            return segment
        idx -= 1
    _warn(f"Unable to derive rule name from {segments} [{orig_idx}]")
    return "<Bug: unknown rule name>"


def is_at(position: types.Position, lc_pos: Tuple[int, int]) -> bool:
    return position.line == lc_pos[0] and position.character == lc_pos[1]


def is_before(position: types.Position, lc_pos: Tuple[int, int]) -> bool:
    line, column = lc_pos
    if position.line < line:
        return True
    if position.line == line and position.character < column:
        return True
    return False


def is_after(position: types.Position, lc_pos: Tuple[int, int]) -> bool:
    line, column = lc_pos
    if position.line > line:
        return True
    if position.line == line and position.character > column:
        return True
    return False


def word_range_at_position(
    lines: List[str],
    line_no: int,
    char_offset: int,
) -> types.Range:
    line = lines[line_no]
    line_len = len(line)
    start_idx = char_offset
    end_idx = char_offset
    while end_idx + 1 < line_len and not line[end_idx + 1].isspace():
        end_idx += 1

    while start_idx - 1 >= 0 and not line[start_idx - 1].isspace():
        start_idx -= 1

    return types.Range(
        types.Position(line_no, start_idx),
        types.Position(line_no, end_idx),
    )


def _escape(v: str) -> str:
    return '"' + v.replace("\n", "\\n") + '"'


def insert_complete_marker_snippet(
    lines: List[str],
    server_position: types.Position,
) -> bool:
    _info(f"Complete at {server_position}")
    line_no = server_position.line
    line = lines[line_no] if line_no < len(lines) else ""
    pos_rhs = (
        line[server_position.character :]
        if server_position.character < len(line)
        else ""
    )
    if pos_rhs and not pos_rhs.isspace():
        _info(f"No insertion: {_escape(line[server_position.character:])}")
        return False
    lhs_ws = line[: server_position.character]
    lhs = lhs_ws.strip()
    if lhs.endswith(":"):
        _info("Insertion of value (key seen)")
        new_line = line[: server_position.character] + YAML_COMPLETION_HINT_VALUE + "\n"
    elif lhs.startswith("-"):
        _info("Insertion of key or value (list item)")
        # Respect the provided indentation
        snippet = (
            YAML_COMPLETION_HINT_KEY if ":" not in lhs else YAML_COMPLETION_HINT_VALUE
        )
        new_line = line[: server_position.character] + snippet + "\n"
    elif not lhs or (lhs_ws and not lhs_ws[0].isspace()):
        _info(f"Insertion of key or value: {_escape(line[server_position.character:])}")
        # Respect the provided indentation
        snippet = (
            YAML_COMPLETION_HINT_KEY if ":" not in lhs else YAML_COMPLETION_HINT_VALUE
        )
        new_line = line[: server_position.character] + snippet + "\n"
    elif lhs.isalpha() and ":" not in lhs:
        _info(f"Expanding value to a key: {_escape(line[server_position.character:])}")
        # Respect the provided indentation
        new_line = line[: server_position.character] + YAML_COMPLETION_HINT_KEY + "\n"
    else:
        c = (
            line[server_position.character]
            if server_position.character < len(line)
            else "(OOB)"
        )
        _info(f"Not touching line: {_escape(line)} -- {_escape(c)}")
        return False
    _info(f'Evaluating complete on synthetic line: "{new_line}"')
    if line_no < len(lines):
        lines[line_no] = new_line
    elif line_no == len(lines):
        lines.append(new_line)
    else:
        return False
    return True


def yaml_key_range(
    key: str,
    line: int,
    col: int,
    lines: List[str],
    position_codec: LintCapablePositionCodec,
) -> types.Range:
    key_len = len(key) if key else 1
    return position_codec.range_to_client_units(
        lines,
        types.Range(
            types.Position(
                line,
                col,
            ),
            types.Position(
                line,
                col + key_len,
            ),
        ),
    )


def yaml_flag_unknown_key(
    key: Optional[str],
    expected_keys: Iterable[str],
    line: int,
    col: int,
    lines: List[str],
    position_codec: LintCapablePositionCodec,
    *,
    message_format: str = 'Unknown or unsupported key "{key}".',
    unknown_keys_diagnostic_severity: Optional[
        types.DiagnosticSeverity
    ] = types.DiagnosticSeverity.Error,
) -> Tuple[Optional[types.Diagnostic], Optional[str]]:
    key_range = yaml_key_range(key, line, col, lines, position_codec)

    candidates = detect_possible_typo(key, expected_keys) if key is not None else ()
    extra = ""
    corrected_key = None
    if candidates:
        extra = f' It looks like a typo of "{candidates[0]}".'
        # TODO: We should be able to tell that `install-doc` and `install-docs` are the same.
        #  That would enable this to work in more cases.
        corrected_key = candidates[0] if len(candidates) == 1 else None
        if unknown_keys_diagnostic_severity is None:
            message_format = f"Possible typo of {candidates[0]}."
            extra = ""
    elif unknown_keys_diagnostic_severity is None:
        return None, None

    if key is None:
        message_format = "Missing key"
    diagnostic = types.Diagnostic(
        key_range,
        message_format.format(key=key) + extra,
        unknown_keys_diagnostic_severity,
        source="debputy",
        data=DiagnosticData(
            quickfixes=[propose_correct_text_quick_fix(n) for n in candidates]
        ),
    )
    return diagnostic, corrected_key
