# SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
# SPDX-License-Identifier: GPL-2.0-or-later
"""Parse the output of `gpg --list-keys --with-colons`."""

from __future__ import annotations

import dataclasses
import typing

import pyparsing as pyp

from testsigs import defs


if typing.TYPE_CHECKING:
    from typing import Final


_p_eol = pyp.Char("\r\n").suppress()

_p_ign = pyp.ZeroOrMore(pyp.CharsNotIn(":\n")).suppress() + pyp.Char(":").suppress()

_p_ign_all = pyp.CharsNotIn("\n").suppress()

_p_hex_string = pyp.Word("0123456789ABCDEF")

_p_string = pyp.CharsNotIn(":\n")

_p_ignored_line = pyp.Literal("tru:").suppress() + _p_ign_all + _p_eol

_p_ignored_lines = pyp.ZeroOrMore(_p_ignored_line)

_p_list_pub = (
    pyp.Literal("pub:").suppress() + _p_ign + _p_ign + _p_ign + _p_hex_string + _p_ign_all + _p_eol
)

_p_list_fpr = pyp.Literal("fpr:").suppress() + _p_ign * 8 + _p_hex_string + _p_ign_all + _p_eol

_p_list_uid = pyp.Literal("uid:").suppress() + _p_ign * 8 + _p_string + _p_ign_all + _p_eol

_p_list_sub = (
    pyp.Literal("sub:").suppress()
    + _p_ign
    + _p_ign
    + _p_ign
    + _p_hex_string
    + _p_ign * 7
    + _p_string
    + _p_ign_all
    + _p_eol
)


@_p_list_sub.set_parse_action
def _parse_list_sub(tokens: pyp.ParseResults) -> defs.Subkey:
    """Parse a subkey definition."""
    params: Final = tokens.as_list()
    match params:
        case [key_id, caps]:
            return defs.Subkey(key_id=key_id, fpr="", capabilities=set(caps))

        case _:
            raise RuntimeError(repr(params))


_p_list_subkey = _p_ignored_lines + _p_list_sub + _p_ignored_lines + _p_list_fpr


@_p_list_subkey.set_parse_action
def _parse_list_subkey(tokens: pyp.ParseResults) -> defs.Subkey:
    """Parse a subkey definition."""
    params: Final = tokens.as_list()
    match params:
        case [sub_key_id, fpr]:
            return dataclasses.replace(sub_key_id, fpr=fpr)

        case _:
            raise RuntimeError(repr(params))


_p_list_pubkey = (
    _p_ignored_lines
    + _p_list_pub
    + _p_ignored_lines
    + _p_list_fpr
    + _p_ignored_lines
    + _p_list_uid
    + pyp.OneOrMore(_p_list_subkey)
)


@_p_list_pubkey.set_parse_action
def _parse_list_pubkey(tokens: pyp.ParseResults) -> defs.PublicKey:
    """Parse a subkey definition."""
    params: Final = tokens.as_list()
    match params:
        case [key_id, fpr, uid, *subkeys] if isinstance(key_id, str) and isinstance(
            fpr,
            str,
        ) and isinstance(uid, str) and subkeys and all(
            isinstance(subkey, defs.Subkey) for subkey in subkeys
        ):
            # We did just validate that one...
            return defs.PublicKey(
                key_id=key_id,
                fpr=fpr,
                uid=uid,
                subkeys=subkeys,  # type: ignore[arg-type]
            )

        case _:
            raise RuntimeError(repr(params))


_p_list_keys = pyp.ZeroOrMore(_p_list_pubkey) + _p_ignored_lines

_p_list_keys_complete = _p_list_keys.leave_whitespace().parse_with_tabs()


def parse_list_keys(output: str) -> list[defs.PublicKey]:
    """Parse the output of `gpg --list-keys`, extract the info we care about."""
    res: Final = _p_list_keys_complete.parse_string(output, parse_all=True).as_list()
    if not all(isinstance(pkey, defs.PublicKey) for pkey in res):
        raise RuntimeError(repr(res))
    return res
