import dataclasses
from bisect import bisect_left, bisect_right
from collections.abc import Mapping
from typing import (
    TypedDict,
    NotRequired,
    List,
    Any,
    Literal,
    Optional,
    TYPE_CHECKING,
    get_args,
    FrozenSet,
    cast,
    Tuple,
    Sequence,
    TypeVar,
)

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

# These are in order of severity (most important to least important).
#
# Special cases:
#  - "spelling" is a specialized version of "pedantic" for textual spelling mistakes
#    (LSP uses the same severity for both; only `debputy lint` shows a difference
#     between them)
#
LintSeverity = Literal["error", "warning", "informational", "pedantic", "spelling"]

LINT_SEVERITY2LSP_SEVERITY: Mapping[LintSeverity, types.DiagnosticSeverity] = {
    "error": types.DiagnosticSeverity.Error,
    "warning": types.DiagnosticSeverity.Warning,
    "informational": types.DiagnosticSeverity.Information,
    "pedantic": types.DiagnosticSeverity.Hint,
    "spelling": types.DiagnosticSeverity.Hint,
}
NATIVELY_LSP_SUPPORTED_SEVERITIES: FrozenSet[LintSeverity] = cast(
    "FrozenSet[LintSeverity]",
    frozenset(
        {
            "error",
            "warning",
            "informational",
            "pedantic",
        }
    ),
)


_delta = set(get_args(LintSeverity)).symmetric_difference(
    LINT_SEVERITY2LSP_SEVERITY.keys()
)
assert (
    not _delta
), f"LintSeverity and LINT_SEVERITY2LSP_SEVERITY are not aligned. Delta: {_delta}"
del _delta


class DiagnosticData(TypedDict):
    quickfixes: NotRequired[Optional[List[Any]]]
    lint_severity: NotRequired[Optional[LintSeverity]]
    report_for_related_file: NotRequired[str]
    enable_non_interactive_auto_fix: bool


@dataclasses.dataclass(slots=True)
class DiagnosticReport:
    doc_uri: str
    doc_version: int
    diagnostic_report_id: str
    is_in_progress: bool
    diagnostics: List[types.Diagnostic]

    _diagnostic_range_helper: Optional["DiagnosticRangeHelper"] = None

    def diagnostics_in_range(self, text_range: types.Range) -> List[types.Diagnostic]:
        if not self.diagnostics:
            return []
        helper = self._diagnostic_range_helper
        if helper is None:
            helper = DiagnosticRangeHelper(self.diagnostics)
            self._diagnostic_range_helper = helper
        return helper.diagnostics_in_range(text_range)


def _pos_as_tuple(pos: types.Position) -> Tuple[int, int]:
    return pos.line, pos.character


class DiagnosticRangeHelper:

    __slots__ = ("diagnostics", "by_start_index", "by_end_index")

    def __init__(self, diagnostics: List[types.Diagnostic]) -> None:
        self.diagnostics = diagnostics
        self.by_start_index = sorted(
            (
                (_pos_as_tuple(diagnostics[i].range.start), i)
                for i in range(len(diagnostics))
            ),
        )
        self.by_end_index = sorted(
            (
                (_pos_as_tuple(diagnostics[i].range.end), i)
                for i in range(len(diagnostics))
            ),
        )

    def diagnostics_in_range(self, text_range: types.Range) -> List[types.Diagnostic]:
        start_pos = _pos_as_tuple(text_range.start)
        end_pos = _pos_as_tuple(text_range.end)

        try:
            lower_index_limit = _find_gt(
                self.by_end_index,
                start_pos,
                key=lambda t: t[0],
            )[1]
        except NoSuchElementError:
            lower_index_limit = len(self.diagnostics)

        try:
            upper_index_limit = _find_lt(
                self.by_start_index,
                end_pos,
                key=lambda t: t[0],
            )[1]

            upper_index_limit += 1
        except NoSuchElementError:
            upper_index_limit = 0

        return self.diagnostics[lower_index_limit:upper_index_limit]


T = TypeVar("T")


class NoSuchElementError(ValueError):
    pass


def _find_lt(a: Sequence[Any], x: Any, *, key: Any = None):
    "Find rightmost value less than x"
    i = bisect_left(a, x, key=key)
    if i:
        return a[i - 1]
    raise NoSuchElementError


def _find_gt(a: Sequence[Any], x: Any, *, key: Any = None):
    "Find leftmost value greater than x"
    i = bisect_right(a, x, key=key)
    if i != len(a):
        return a[i]
    raise NoSuchElementError
