"""
An implementation of Damm check digits.
"""

import re
from typing import NamedTuple


class DammException(Exception):
    """Base class for all exceptions in the module."""


class CheckDigitException(DammException):
    """Thrown when a string did not check when decoding a supposedly checked string."""


class NoMatchException(DammException):
    """Thrown when a string did not match."""


class Damm(NamedTuple):
    """A Damm coded number."""

    MATRIX = (
        (0, 3, 1, 7, 5, 9, 8, 6, 4, 2),
        (7, 0, 9, 2, 1, 5, 4, 8, 6, 3),
        (4, 2, 0, 6, 8, 7, 1, 3, 5, 9),
        (1, 7, 5, 0, 9, 8, 3, 4, 2, 6),
        (6, 1, 2, 3, 0, 4, 5, 9, 7, 8),
        (3, 6, 7, 4, 2, 0, 9, 5, 8, 1),
        (5, 8, 6, 9, 7, 2, 0, 1, 3, 4),
        (8, 9, 4, 5, 3, 6, 2, 0, 1, 7),
        (9, 4, 3, 8, 6, 1, 7, 2, 0, 5),
        (2, 5, 8, 1, 4, 3, 6, 7, 9, 0),
    )

    DAMM_PATTERN = re.compile(r"(?P<value>\d+)-?(?P<check>\d)")

    value: int

    def __str__(self):
        return f"{self.value}-{self.encode(self.value)}"

    def __repr__(self):
        return f"{self.__class__.__name__}({self.value}, {self.encode(self.value)})"

    @classmethod
    def checkdigit(cls, value: int) -> int:
        """Compute a check digit for a given value."""

        state = 0
        for digit in str(value):
            state = cls.MATRIX[state][int(digit)]
        return state

    @classmethod
    def encode(cls, value: int) -> str:
        """'encode' a value with a check digit."""

        return f"{value}-{cls.checkdigit(value)}"

    @classmethod
    def verify(cls, value: str) -> bool:
        """Verify a Damm encoded number, returning its value if valid.

        The core Damm encoding property is that the LAST digit is ALWAYS the check. When the rest of
        the string is correctly Damm encoded, the Damm of the entire string will be 0.

        """

        return cls.checkdigit(int(value.replace("-", ""))) == 0

    @classmethod
    def from_str(cls, value: str) -> "Damm":
        """Verify a Damm coded string, and return its decoding."""

        if match := re.fullmatch(cls.DAMM_PATTERN, value):
            given_value = match.group("value")
            computed_code = cls.encode(given_value)
            given_code = int(match.group("check"))
            if computed_code == given_code:
                return Damm(int(given_value))
            else:
                raise CheckDigitException(
                    f"Value {value!r} had check digit {given_code!r}, but computed check was {computed_code!r}"
                )
        else:
            raise NoMatchException(f"Input {value!r} was not a valid decimal number")
