import re
from typing import (
    Optional,
    List,
)

from lsprotocol.types import (
    Diagnostic,
    TEXT_DOCUMENT_WILL_SAVE_WAIT_UNTIL,
    Position,
    Range,
    DiagnosticSeverity,
)
from ruamel.yaml.error import MarkedYAMLError, YAMLError

from debputy.highlevel_manifest import MANIFEST_YAML
from debputy.lsp.lsp_features import (
    lint_diagnostics,
    lsp_standard_handler,
)
from debputy.lsp.text_util import (
    LintCapablePositionCodec,
)

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


_CONTAINS_TAB_OR_COLON = re.compile(r"[\t:]")
_WORDS_RE = re.compile("([a-zA-Z0-9_-]+)")
_MAKE_ERROR_RE = re.compile(r"^[^:]+:(\d+):\s*(\S.+)")


_LANGUAGE_IDS = [
    "debian/debputy.manifest",
    "debputy.manifest",
    # LSP's official language ID for YAML files
    "yaml",
]


# lsp_standard_handler(_LANGUAGE_IDS, TEXT_DOCUMENT_CODE_ACTION)
lsp_standard_handler(_LANGUAGE_IDS, TEXT_DOCUMENT_WILL_SAVE_WAIT_UNTIL)


def is_valid_file(path: str) -> bool:
    # For debian/debputy.manifest, the language ID is often set to makefile meaning we get random
    # "non-debian/debputy.manifest" YAML files here. Skip those.
    return path.endswith("debian/debputy.manifest")


def _word_range_at_position(
    lines: List[str],
    line_no: int,
    char_offset: int,
) -> 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 Range(
        Position(line_no, start_idx),
        Position(line_no, end_idx),
    )


@lint_diagnostics(_LANGUAGE_IDS)
def _lint_debian_debputy_manifest(
    _doc_reference: str,
    path: str,
    lines: List[str],
    position_codec: LintCapablePositionCodec,
) -> Optional[List[Diagnostic]]:
    if not is_valid_file(path):
        return None
    diagnostics = []
    try:
        MANIFEST_YAML.load("".join(lines))
    except MarkedYAMLError as e:
        error_range = position_codec.range_to_client_units(
            lines,
            _word_range_at_position(
                lines,
                e.problem_mark.line,
                e.problem_mark.column,
            ),
        )
        diagnostics.append(
            Diagnostic(
                error_range,
                f"YAML parse error: {e}",
                DiagnosticSeverity.Error,
            ),
        )
    except YAMLError as e:
        error_range = position_codec.range_to_client_units(
            lines,
            Range(
                Position(0, 0),
                Position(0, len(lines[0])),
            ),
        )
        diagnostics.append(
            Diagnostic(
                error_range,
                f"Unknown YAML parse error: {e} [{e!r}]",
                DiagnosticSeverity.Error,
            ),
        )

    return diagnostics
