import re
from email.utils import parsedate_to_datetime

from debputy.linting.lint_util import LintState
from debputy.lsp.lsp_features import (
    lsp_standard_handler,
    SecondaryLanguage,
    LanguageDispatchRule,
    lint_diagnostics,
)
from debputy.lsp.quickfixes import (
    propose_correct_text_quick_fix,
)
from debputy.lsp.spellchecking import spellcheck_line
from debputy.lsprotocol.types import (
    TEXT_DOCUMENT_WILL_SAVE_WAIT_UNTIL,
    TEXT_DOCUMENT_CODE_ACTION,
)
from debputy.util import PKGVERSION_REGEX

try:
    from debputy.lsp.vendoring._deb822_repro.locatable import (
        Position as TEPosition,
        Range as TERange,
    )

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


# Same as Lintian
_MAXIMUM_WIDTH: int = 82
_HEADER_LINE = re.compile(r"^(\S+)\s*[(]([^)]+)[)]")  # TODO: Add rest
_DISPATCH_RULE = LanguageDispatchRule.new_rule(
    "debian/changelog",
    ("debian/changelog", "debian/changelog.dch"),
    [
        # emacs's name
        SecondaryLanguage("debian-changelog"),
        # vim's name
        SecondaryLanguage("debchangelog"),
        SecondaryLanguage("dch"),
    ],
)


_WEEKDAYS_BY_IDX = [
    "Mon",
    "Tue",
    "Wed",
    "Thu",
    "Fri",
    "Sat",
    "Sun",
]
_KNOWN_WEEK_DAYS = frozenset(_WEEKDAYS_BY_IDX)


lsp_standard_handler(_DISPATCH_RULE, TEXT_DOCUMENT_CODE_ACTION)
lsp_standard_handler(_DISPATCH_RULE, TEXT_DOCUMENT_WILL_SAVE_WAIT_UNTIL)


def _check_footer_line(
    lint_state: LintState,
    line: str,
    line_no: int,
) -> None:
    try:
        end_email_idx = line.rindex(">  ")
    except ValueError:
        # Syntax error; flag later
        return
    line_len = len(line)
    start_date_idx = end_email_idx + 3
    # 3 characters for the day name (Mon), then a comma plus a space followed by the
    # actual date. The 6 characters limit is a gross under estimation of the real
    # size.
    if line_len < start_date_idx + 6:
        text_range = _single_line_subrange(line_no, start_date_idx, line_len)
        lint_state.emit_diagnostic(
            text_range,
            "Expected a date in RFC822 format (Tue, 12 Mar 2024 12:34:56 +0000)",
            "error",
            "debputy",
        )
        return
    day_name_range = _single_line_subrange(line_no, start_date_idx, start_date_idx + 3)
    day_name = line[start_date_idx : start_date_idx + 3]
    if day_name not in _KNOWN_WEEK_DAYS:
        lint_state.emit_diagnostic(
            day_name_range,
            "Expected a three letter date here using US English format (Mon, Tue, ..., Sun).",
            "error",
            "debputy",
        )
        return

    date_str = line[start_date_idx + 5 :]

    if line[start_date_idx + 3 : start_date_idx + 5] != ", ":
        sep = line[start_date_idx + 3 : start_date_idx + 5]
        text_range = _single_line_subrange(
            line_no,
            start_date_idx + 3,
            start_date_idx + 4,
        )
        lint_state.emit_diagnostic(
            text_range,
            f'Improper formatting of date. Expected ", " here, not "{sep}"',
            "error",
            "debputy",
        )
        return

    try:
        # FIXME: this parser is too forgiving (it ignores trailing garbage)
        date = parsedate_to_datetime(date_str)
    except ValueError as e:
        error_range = _single_line_subrange(line_no, start_date_idx + 5, line_len)
        lint_state.emit_diagnostic(
            error_range,
            f"Unable to the date as a valid RFC822 date: {e.args[0]}",
            "error",
            "debputy",
        )
        return
    expected_week_day = _WEEKDAYS_BY_IDX[date.weekday()]
    if expected_week_day != day_name:
        lint_state.emit_diagnostic(
            day_name_range,
            f"The date was a {expected_week_day}day.",
            "warning",
            "debputy",
            quickfixes=[propose_correct_text_quick_fix(expected_week_day)],
        )


def _single_line_subrange(
    line_no: int,
    character_start_pos: int,
    character_end_pos: int,
) -> "TERange":
    return TERange(
        TEPosition(
            line_no,
            character_start_pos,
        ),
        TEPosition(
            line_no,
            character_end_pos,
        ),
    )


def _check_header_line(
    lint_state: LintState,
    line: str,
    line_no: int,
    entry_no: int,
) -> None:
    m = _HEADER_LINE.search(line)
    if not m:
        # Syntax error: TODO flag later
        return
    source_name, source_version = m.groups()
    dctrl_source_pkg = lint_state.source_package
    if (
        entry_no == 1
        and dctrl_source_pkg is not None
        and dctrl_source_pkg.fields.get("Source") != source_name
    ):
        expected_name = dctrl_source_pkg.fields.get("Source")
        start_pos, end_pos = m.span(1)
        name_range = _single_line_subrange(line_no, start_pos, end_pos)
        if expected_name is None:
            msg = (
                "The first entry must use the same source name as debian/control."
                ' The d/control file is missing the "Source" field in its first stanza'
            )
        else:
            msg = (
                "The first entry must use the same source name as debian/control."
                f' Changelog uses: "{source_name}" while d/control uses: "{expected_name}"'
            )

        lint_state.emit_diagnostic(
            name_range,
            msg,
            "error",
            "dpkg",  # man:deb-src-control(5) / #1089794
        )
    if not PKGVERSION_REGEX.fullmatch(source_version):
        vm = PKGVERSION_REGEX.search(source_version)
        start_pos, end_pos = m.span(2)
        if vm:
            start_valid, end_valid = vm.span(0)
            invalid_ranges = []
            if start_valid > 0:
                name_range = _single_line_subrange(
                    line_no,
                    start_pos,
                    start_pos + start_valid,
                )
                invalid_ranges.append(name_range)

            if end_valid < len(source_version):
                name_range = _single_line_subrange(
                    line_no,
                    start_pos + end_valid,
                    end_pos,
                )
                invalid_ranges.append(name_range)

            for r in invalid_ranges:
                lint_state.emit_diagnostic(
                    r,
                    "This part cannot be parsed as a valid Debian version.",
                    "error",
                    "Policy 5.6.12",
                )
        else:
            name_range = _single_line_subrange(line_no, start_pos, end_pos)
            lint_state.emit_diagnostic(
                name_range,
                f'Cannot parse "{source_version}" as a Debian version.',
                "error",
                "Policy 5.6.12",
            )
    elif "dsfg" in source_version:
        typo_index = source_version.index("dsfg")
        start_pos, end_pos = m.span(2)

        name_range = _single_line_subrange(
            line_no,
            start_pos + typo_index,
            start_pos + typo_index + 4,
        )
        lint_state.emit_diagnostic(
            name_range,
            'Typo of "dfsg" (Debian Free Software Guidelines)',
            "pedantic",
            "debputy",
            quickfixes=[propose_correct_text_quick_fix("dfsg")],
        )


@lint_diagnostics(_DISPATCH_RULE)
async def _lint_debian_changelog(lint_state: LintState) -> None:
    lines = lint_state.lines
    entry_no = 0
    entry_limit = 2
    max_words = 1000
    max_line_length = _MAXIMUM_WIDTH
    for line_no, line in enumerate(lines):
        orig_line = line
        line = line.rstrip()
        if not line:
            continue
        if line.startswith(" --"):
            _check_footer_line(lint_state, line, line_no)
            continue
        if not line.startswith("  "):
            if not line[0].isspace():
                entry_no += 1
                # Figure out the right cut which may not be as simple as just the
                # top two.
                if entry_no > entry_limit:
                    break
                _check_header_line(
                    lint_state,
                    line,
                    line_no,
                    entry_no,
                )
            continue
        # minus 1 for newline
        orig_line_len = len(orig_line) - 1
        if orig_line_len > max_line_length:
            exceeded_line_range = _single_line_subrange(
                line_no,
                max_line_length,
                orig_line_len,
            )
            lint_state.emit_diagnostic(
                exceeded_line_range,
                f"Line exceeds {max_line_length} characters",
                "pedantic",
                "debputy",
            )
        if len(line) > 3 and line[2] == "[" and line[-1] == "]":
            # Do not spell check [ X ] as X is usually a name
            continue
        if max_words > 0:
            new_diagnostics = spellcheck_line(lint_state, line_no, line)
            max_words -= new_diagnostics
