import textwrap
from functools import lru_cache
from typing import (
    Optional,
    Any,
    Union,
    Sequence,
    TYPE_CHECKING,
)

from debputy.highlevel_manifest import MANIFEST_YAML
from debputy.linting.lint_util import LintState, te_range_to_lsp
from debputy.lsp.lsp_features import (
    lint_diagnostics,
    lsp_standard_handler,
    lsp_hover,
    lsp_completer,
    LanguageDispatchRule,
    SecondaryLanguage,
)
from debputy.lsp.lsp_generic_yaml import (
    error_range_at_position,
    insert_complete_marker_snippet,
    YAML_COMPLETION_HINT_KEY,
    yaml_flag_unknown_key,
    _trace_cursor,
    DEBPUTY_PLUGIN_METADATA,
    resolve_keyword,
    generic_yaml_hover,
    completion_from_attr,
)
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 (
    CommentedMap,
    CommentedSeq,
    MarkedYAMLError,
    YAMLError,
)

try:
    from debputy.lsp.debputy_ls import DebputyLanguageServer
    from debputy.lsp.vendoring._deb822_repro.locatable import (
        Position as TEPosition,
        Range as TERange,
    )
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="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)
async def _lint_debian_upstream_metadata(
    lint_state: LintState,
) -> None:
    lines = lint_state.lines

    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
        else:
            line = e.problem_mark.line
            column = e.problem_mark.column
        error_range = error_range_at_position(
            lines,
            line,
            column,
        )
        lint_state.emit_diagnostic(
            error_range,
            f"YAML parse error: {e}",
            "error",
            "debputy",
        )
    except YAMLError as e:
        error_range = TERange(
            TEPosition(0, 0),
            TEPosition(0, len(lines[0])),
        )
        lint_state.emit_diagnostic(
            error_range,
            f"Unknown YAML parse error: {e} [{e!r}]",
            "error",
            "debputy",
        )
    else:
        feature_set = lint_state.plugin_feature_set
        pg = feature_set.manifest_parser_generator
        root_parser = root_object_parser()
        _lint_content(
            lint_state,
            pg,
            root_parser,
            content,
        )


def _conflicting_key(
    lint_state: LintState,
    key_a: str,
    key_b: str,
    key_a_line: int,
    key_a_col: int,
    key_b_line: int,
    key_b_col: int,
) -> None:
    key_a_range = TERange(
        TEPosition(
            key_a_line,
            key_a_col,
        ),
        TEPosition(
            key_a_line,
            key_a_col + len(key_a),
        ),
    )
    key_b_range = TERange(
        TEPosition(
            key_b_line,
            key_b_col,
        ),
        TEPosition(
            key_b_line,
            key_b_col + len(key_b),
        ),
    )
    lint_state.emit_diagnostic(
        key_a_range,
        f'The "{key_a}" cannot be used with "{key_b}".',
        "error",
        "debputy",
        related_information=[
            lint_state.related_diagnostic_information(
                key_b_range, f'The attribute "{key_b}" is used here.'
            ),
        ],
    )

    lint_state.emit_diagnostic(
        key_b_range,
        f'The "{key_b}" cannot be used with "{key_a}".',
        "error",
        "debputy",
        related_information=[
            lint_state.related_diagnostic_information(
                key_a_range,
                f'The attribute "{key_a}" is used here.',
            ),
        ],
    )


def _lint_attr_value(
    lint_state: LintState,
    attr: AttributeDescription,
    pg: ParserGenerator,
    value: Any,
) -> None:
    attr_type = attr.attribute_type
    if isinstance(attr_type, type) and issubclass(attr_type, DebputyDispatchableType):
        parser = pg.dispatch_parser_table_for(attr_type)
        _lint_content(
            lint_state,
            pg,
            parser,
            value,
        )


def _lint_declarative_mapping_input_parser(
    lint_state: LintState,
    pg: ParserGenerator,
    parser: DeclarativeMappingInputParser,
    content: Any,
) -> None:
    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:
            corrected_key = yaml_flag_unknown_key(
                lint_state,
                key,
                parser.manifest_attributes,
                line,
                col,
            )
            if corrected_key:
                key = corrected_key
                attr = parser.manifest_attributes.get(corrected_key)
        if attr is None:
            continue

        _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)
                _conflicting_key(
                    lint_state,
                    key,
                    forbidden_key,
                    line,
                    col,
                    con_line,
                    con_col,
                )
    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)
            _conflicting_key(
                lint_state,
                key,
                other,
                line,
                col,
                con_line,
                con_col,
            )


def _lint_content(
    lint_state: LintState,
    pg: ParserGenerator,
    parser: DeclarativeInputParser[Any],
    content: Any,
) -> None:
    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)
                corrected_key = yaml_flag_unknown_key(
                    lint_state,
                    key,
                    parser.registered_keywords(),
                    line,
                    col,
                    unknown_keys_diagnostic_severity=parser.unknown_keys_diagnostic_severity,
                )
                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
                _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:
            _lint_content(lint_state, pg, subparser, value)
    elif isinstance(parser, InPackageContextParser):
        if not isinstance(content, CommentedMap):
            return
        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)
                yaml_flag_unknown_key(
                    lint_state,
                    k,
                    known_packages,
                    line,
                    col,
                    message_format='Unknown package "{key}".',
                )
            _lint_content(lint_state, pg, parser.delegate, v)
    elif isinstance(parser, DeclarativeMappingInputParser):
        _lint_declarative_mapping_input_parser(
            lint_state,
            pg,
            parser,
            content,
        )


@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


@lsp_hover(_DISPATCH_RULE)
def debputy_manifest_hover(
    ls: "DebputyLanguageServer",
    params: types.HoverParams,
) -> Optional[types.Hover]:
    return generic_yaml_hover(ls, params, lambda _: root_object_parser())
