import re
import string
from datetime import date, datetime, time
from decimal import Decimal
from enum import IntEnum, StrEnum, auto
from logging import Logger
from typing import Any, Final

from .datetime_pomes import TIMEZONE_LOCAL
from .env_pomes import APP_PREFIX, env_get_str, env_get_enum
from .str_pomes import (
    str_as_list, str_sanitize, str_find_char, str_find_whitespace
)


class MsgLang(StrEnum):
    """
    Possible languages for error reporting.
    """
    EN = auto()
    PT = auto()


VALIDATION_MSG_LANGUAGE: Final[MsgLang] = env_get_enum(key=f"{APP_PREFIX}_VALIDATION_MSG_LANGUAGE",
                                                       enum_class=MsgLang,
                                                       use_names=False,
                                                       def_value=MsgLang.EN)
VALIDATION_MSG_PREFIX: Final[str] = env_get_str(key=f"{APP_PREFIX}_VALIDATION_MSG_PREFIX",
                                                def_value=APP_PREFIX)


def validate_value(attr: str,
                   val: str | float | Decimal,
                   min_val: int = None,
                   max_val: int = None,
                   values: list = None,
                   ignore_case: bool = False,
                   required: bool = False) -> str | None:
    """
    Validate *val* according to value, range, or membership in values list, as specified.

    :param attr: the name of the attribute
    :param val: the value to be validated
    :param min_val: if *val* is a string, specifies its minimum length; otherwise, specifies its minimum value
    :param max_val: if *val* is a string, specifies its maximum length; otherwise, specifies its maximum value
    :param values: if provided, requires *val* to be contained therein
    :param ignore_case: specifies whether to ignore capitalization when handling string values
    :param required:  requires *val* to be specified
    :return: *None* if *val* passes validation, or the corresponding error message otherwise
    """
    # initialize the return variable
    result: str | None = None

    if val is None or val == "":
        if isinstance(required, bool) and required:
            # 121: Required attribute
            result = validate_format_error(121,
                                           f"@{attr}")
    elif isinstance(values, list):
        if ignore_case:
            val = val.lower() if isinstance(val, str) else val
            values = [v.lower() if isinstance(v, str) else v for v in values]
        if val not in values:
            length: int = len(values)
            if length == 1:
                # 149: Invalid value {}: must be {}
                result = validate_format_error(149,
                                               val,
                                               values[0],
                                               f"@{attr}")
            else:
                # 150: Invalid value {}: must be one of {}
                result = validate_format_error(150,
                                               val,
                                               values[:length],
                                               f"@{attr}")
    elif isinstance(val, str):
        length: int = len(val)
        if min_val is not None and max_val == min_val and length != min_val:
            # 146: Invalid value {}: length must be {}
            result = validate_format_error(156,
                                           val,
                                           min_val,
                                           f"@{attr}")
        elif max_val is not None and max_val < length:
            # 148: Invalid value {}: length longer than {}
            result = validate_format_error(148,
                                           val,
                                           max_val,
                                           f"@{attr}")
        elif min_val is not None and length < min_val:
            # 147: Invalid value {}: length shorter than {}
            result = validate_format_error(147,
                                           val,
                                           min_val,
                                           f"@{attr}")
    elif ((min_val is not None and val < min_val) or
          (max_val is not None and val > max_val)):
        if min_val is not None and max_val is not None:
            # 151: Invalid value {}: must be in the range {}
            result = validate_format_error(151,
                                           val,
                                           [min_val, max_val],
                                           f"@{attr}")
        elif min_val is not None:
            # 144: Invalid value {}: must be greater than {}
            result = validate_format_error(144,
                                           val,
                                           min_val,
                                           f"@{attr}")
        else:
            # 143: Invalid value {}: must be less than {}
            result = validate_format_error(143,
                                           val,
                                           max_val,
                                           f"@{attr}")
    return result


def validate_bool(errors: list[str] | None,
                  source: dict[str, Any],
                  attr: str,
                  default: bool = None,
                  required: bool = False,
                  logger: Logger = None) -> bool | None:
    """
    Validate the boolean value associated with *attr* in *source*.

    If provided, this value must be on of:
        - a *bool*
        - the integer *1* or *0*
        - the string *1*, *t*, or *true*, case disregarded
        - the string *0*, *f*, or *false*, case disregarded

    :param errors: incidental error messages
    :param source: *dict* containing the value to be validated
    :param attr: the name of the attribute whose value is being validated
    :param default: default value, overrides *required*
    :param required: specifies whether a value must be provided
    :param logger: optional logger
    :return: the validated value, or *None* if validation failed
    """
    # initialize the return variable
    result: bool | None = None

    stat: str | None = None
    pos: int = attr.rfind(".") + 1
    suffix: str = attr[pos:]

    # retrieve the value
    value = source.get(suffix)

    # validate it
    if value is None or value == "":
        if default is not None:
            value = default
        elif required:
            # 121: Required attribute
            stat = validate_format_error(121,
                                         f"@{attr}")
    elif isinstance(value, str):
        if value.lower() in ["1", "t", "true"]:
            value = True
        elif value.lower() in ["0", "f", "false"]:
            value = False
        else:
            # 152: Invalid value {}: must be type {}
            stat = validate_format_error(152,
                                         value,
                                         "bool",
                                         f"@{attr}")
    # bool is subtype of int
    elif isinstance(value, int) and not isinstance(value, bool):
        if value == 1:
            value = True
        elif value == 0:
            value = False
        else:
            # 152: Invalid value {}: must be type {}
            stat = validate_format_error(152,
                                         value,
                                         "bool",
                                         f"@{attr}")
    elif not isinstance(value, bool):
        # 152: Invalid value {}: must be type {}
        stat = validate_format_error(152,
                                     value,
                                     "bool",
                                     f"@{attr}")
    if stat:
        if logger:
            logger.error(msg=stat)
        if isinstance(errors, list):
            errors.append(stat)
    else:
        result = value

    return result


def validate_int(errors: list[str] | None,
                 source: dict[str, Any],
                 attr: str,
                 min_val: int = None,
                 max_val: int = None,
                 values: list[int] = None,
                 default: int = None,
                 required: bool = False,
                 logger: Logger = None) -> int | None:
    """
    Validate the *int* value associated with *attr* in *source*.

    If provided, this value must be a *int*, or a valid string representation of a *int*.

    :param errors: incidental error messages
    :param source: *dict* containing the value to be validated
    :param attr: the attribute associated with the value to be validated
    :param min_val: the minimum value accepted
    :param max_val:  the maximum value accepted
    :param values: optional list of allowed values
    :param default: optional default value, overrides *required*
    :param required: specifies whether a value must be provided
    :param logger: optional logger
    :return: the validated value, or *None* if validation failed
    """
    # initialize the return variable
    result: int | None = None

    stat: str | None = None
    pos: int = attr.rfind(".") + 1
    suffix: str = attr[pos:]

    # retrieve the value
    value: int = source.get(suffix)

    # validate it ('bool' is subtype of 'int')
    if value is None and isinstance(default, int) and not isinstance(default, bool):
        value = default
    elif isinstance(value, str) and value.isnumeric():
        value = int(value)
    elif value is not None and \
            (isinstance(value, bool) or not isinstance(value, int)):
        # 152: Invalid value {}: must be type {}
        stat = validate_format_error(152,
                                     value,
                                     "int",
                                     f"@{attr}")
    if not stat:
        stat = validate_value(attr=attr,
                              val=value,
                              min_val=min_val,
                              max_val=max_val,
                              values=values,
                              required=required)
    if stat:
        if logger:
            logger.error(msg=stat)
        if isinstance(errors, list):
            errors.append(stat)
    else:
        result = value

    return result


def validate_decimal(errors: list[str] | None,
                     source: dict[str, Any],
                     attr: str,
                     min_val: float = None,
                     max_val: float = None,
                     required: bool = False,
                     values: list[float | int] = None,
                     default: float = None,
                     logger: Logger = None) -> Decimal | None:
    """
    Validate the *float* value associated with *attr* in *source*.

    If provided, this value must be a *float*, or a valid string representation of a *float*.

    :param errors: incidental error messages
    :param source: *dict* containing the value to be validated
    :param attr: the attribute associated with the value to be validated
    :param min_val: the minimum value accepted
    :param max_val:  the maximum value accepted
    :param values: optional list of allowed values
    :param default: optional default value, overrides *required*
    :param required: specifies whether a value must be provided
    :param logger: optional logger
    :return: the validated value, or *None* if validation failed
    """
    # initialize the return variable
    result: Decimal | None = None

    stat: str | None = None
    pos: int = attr.rfind(".") + 1
    suffix: str = attr[pos:]

    # retrieve the value
    value: float = source.get(suffix)

    # validate it
    if value is None and isinstance(default, int | float | Decimal):
        value = Decimal(default)
    elif (isinstance(value, float) or
          (isinstance(value, int) and not isinstance(value, bool)) or
          (isinstance(value, str) and value.replace(".", "", 1).isnumeric())):
        value = Decimal(value)
    elif isinstance(value, bool) or \
            (value is not None and not isinstance(value, int | float | Decimal)):
        # 152: Invalid value {}: must be type {}
        stat = validate_format_error(152,
                                     value,
                                     "decimal",
                                     f"@{attr}")
    if not stat:
        stat = validate_value(attr=attr,
                              val=value,
                              min_val=min_val,
                              max_val=max_val,
                              values=values,
                              required=required)
    if stat:
        if logger:
            logger.error(msg=stat)
        if isinstance(errors, list):
            errors.append(stat)
    else:
        result = value

    return result


def validate_str(errors: list[str] | None,
                 source: dict[str, Any],
                 attr: str,
                 min_length: int = None,
                 max_length: int = None,
                 values: list[str] = None,
                 default: str = None,
                 ignore_case: bool = False,
                 required: bool = False,
                 logger: Logger = None) -> str | None:
    """
    Validate the *str* value associated with *attr* in *source*.

    If provided, this value must be a *str*.

    :param errors: incidental error messages
    :param source: *dict* containing the value to be validated
    :param attr: the attribute associated with the value to be validated
    :param min_length: optional minimum length accepted
    :param max_length:  optional maximum length accepted
    :param values: optional list of allowed values
    :param default: optional default value, overrides *required*
    :param ignore_case: specifies whether to ignore capitalization
    :param required: specifies whether a value must be provided
    :param logger: optional logger
    :return: the validated value, or *None* if validation failed
    """
    # initialize the return variable
    result: str | None = None

    stat: str | None = None
    pos: int = attr.rfind(".") + 1
    suffix: str = attr[pos:]

    # obtain the value
    value: str | None = source.get(suffix)

    # validate it
    if value is None and isinstance(default, str):
        value = default
    elif value is not None and not isinstance(value, str):
        # 152: Invalid value {}: must be type {}
        stat = validate_format_error(152,
                                     value,
                                     "str",
                                     f"@{attr}")
    else:
        stat = validate_value(attr=attr,
                              val=value,
                              min_val=min_length,
                              max_val=max_length,
                              values=values,
                              ignore_case=ignore_case,
                              required=required)
    if stat:
        if logger:
            logger.error(msg=stat)
        if isinstance(errors, list):
            errors.append(stat)
    else:
        result = value

    return result


def validate_date(errors: list[str] | None,
                  source: dict[str, Any],
                  attr: str,
                  day_first: bool = False,
                  default: date = None,
                  required: bool = False,
                  logger: Logger = None) -> date | None:
    """
    Validate the *date* value associated with *attr* in *source*.

    If provided, this value must be a *date* or *datetime*, a valid string representation of
    a *date* or *datetime*, or an integer or float encoding a UNIX-style *timestamp*.
    If the value obtained is a *datetime*, its *date* part is returned.

    :param errors: incidental error messages
    :param source: *dict* containing the value to be validated
    :param attr: the attribute associated with the value to be validated
    :param day_first: indicates that the day precedes the month in the string representing the date
    :param default: optional default value, overrides *required*
    :param required: specifies whether a value must be provided
    :param logger: optional logger
    :return: the validated value, or *None* if validation failed
    """
    # import needed module
    from .datetime_pomes import date_parse

    # initialize the return variable
    result: date | None = None

    stat: str | None = None
    pos: int = attr.rfind(".") + 1
    suffix: str = attr[pos:]

    # obtain the value
    value: str = source.get(suffix)

    # validate it
    if value:
        if isinstance(value, datetime):
            result = value.date()
        elif isinstance(value, date):
            result = value
        elif isinstance(value, int | float):
            result = datetime.fromtimestamp(value).date()
        else:
            result = date_parse(dt_str=value,
                                dayfirst=day_first)
        if not result:
            # 141: Invalid value {}
            stat = validate_format_error(141,
                                         value,
                                         f"@{attr}")
        elif result > datetime.now(tz=TIMEZONE_LOCAL).date():
            # 153: Invalid value {}: date is later than the current date
            stat = validate_format_error(153,
                                         value,
                                         f"@{attr}")
    elif isinstance(default, date):
        result = default
    elif isinstance(required, bool) and required:
        # 121: Required attribute
        stat = validate_format_error(121,
                                     f"@{attr}")
    if stat:
        result = None
        if logger:
            logger.error(msg=stat)
        if isinstance(errors, list):
            errors.append(stat)

    return result


def validate_datetime(errors: list[str] | None,
                      source: dict[str, Any],
                      attr: str,
                      day_first: bool = True,
                      default: datetime = None,
                      required: bool = False,
                      logger: Logger = None) -> datetime | None:
    """
    Validate the *datetime* value associated with *attr* in *source*.

    If provided, this value must be a *date* or *datetime*, a valid string representation of
    a *date* or *datetime*, or an integer or float encoding a UNIX-style timestamp.
    If the value obtained is a *date*, it is converted to a *datetime* by appending *00:00:00*.

    :param errors: incidental error messages
    :param source: *dict* containing the value to be validated
    :param attr: the attribute associated with the value to be validated
    :param day_first: indicates that the day precedes the month in the string representing the date
    :param default: optional default value, overrides *required*
    :param required: specifies whether a value must be provided
    :param logger: optional logger
    :return: the validated value, or *None* if validation failed
    """
    # import needed module
    from .datetime_pomes import datetime_parse

    # initialize the return variable
    result: datetime | None = None

    stat: str | None = None
    pos: int = attr.rfind(".") + 1
    suffix: str = attr[pos:]

    # obtain and validate the value
    value: str = source.get(suffix)
    if value:
        if isinstance(value, datetime):
            result = value
        elif isinstance(value, date):
            result = datetime.combine(result, time())
        elif isinstance(value, int | float):
            result = datetime.fromtimestamp(value)
        else:
            result = datetime_parse(dt_str=value,
                                    dayfirst=day_first)
        if not result:
            # 141: Invalid value {}
            stat = validate_format_error(141,
                                         value,
                                         f"@{attr}")
        elif result > datetime.now(tz=TIMEZONE_LOCAL):
            # 153: Invalid value {}: date is later than the current date
            stat = validate_format_error(153,
                                         value,
                                         f"@{attr}")
    elif isinstance(default, datetime):
        result = default
    elif isinstance(required, bool) and required:
        # 121: Required attribute
        stat = validate_format_error(121,
                                     f"@{attr}")
    if stat:
        if logger:
            logger.error(msg=stat)
        if isinstance(errors, list):
            errors.append(stat)

    return result


def validate_enum(errors: list[str] | None,
                  source: dict[str, Any],
                  attr: str,
                  enum_class: type[IntEnum | StrEnum],
                  use_names: bool = False,
                  values: list[IntEnum | StrEnum] = None,
                  default: IntEnum | StrEnum = None,
                  required: bool = False,
                  logger: Logger = None) -> Any:
    """
    Validate the *enum* value associated with *attr* in *source*.

    If provided, this value must be a name or a value corresponding to an instance of a subclass of *enum_class*.
    The only accepted values for *enum_class* are subclasses of  *StrEnum* or *IntEnum*.
    The parameter *use_names* determines whether the names of the elements in the *enum_class* should be used,
    ignoring capitalization. If not specified, the values of the elements are used (the default).
    If *values* is specified, the value obtained is checked for occurrence therein.

    :param errors: incidental error messages
    :param source: *dict* containing the value to be validated
    :param attr: the attribute associated with the value to be validated
    :param enum_class: the *enum* class to consider (must be a subclass of *IntEnum* or *StrEnum*)
    :param use_names: specifies whether *enum*'s names should be used (defaults to using *enum*'s values)
    :param default: optional default value, overrides *required*
    :param values: optional list of allowed values (defaults to all elements of *enum_class*)
    :param required: specifies whether a value must be provided
    :param logger: optional logger
    :return: the validated value as an instance of *enum_class*, or *None* if validation failed
    """
    # initialize the return variable
    result: Any = None

    if use_names:
        name: str = validate_str(errors=errors,
                                 source=source,
                                 attr=attr,
                                 values=[e.name for e in (values or enum_class)],
                                 default=default.name if default else None,
                                 ignore_case=True,
                                 required=required,
                                 logger=logger)
        if name:
            for e in enum_class:
                if e.name.lower() == name.lower():
                    result = e
                    break
    else:
        value: Any = None
        vals: list = [e.value for e in (values or enum_class)]
        if issubclass(enum_class, StrEnum):
            value: str = validate_str(errors=errors,
                                      source=source,
                                      attr=attr,
                                      values=vals,
                                      default=default,
                                      required=required,
                                      logger=logger)
        elif issubclass(enum_class, IntEnum):
            value: int = validate_int(errors=errors,
                                      source=source,
                                      attr=attr,
                                      values=vals,
                                      default=default,
                                      required=required,
                                      logger=logger)
        if value:
            result = enum_class(value)

    return result


def validate_email(errors: list[str] | None,
                   source: dict[str, Any],
                   attr: str,
                   default: str = None,
                   required: bool = False,
                   logger: Logger = None) -> IntEnum | StrEnum | None:
    """
    Validate the email value associated with *attr* in *source*.

    If provided, this value must be a syntactically correct email.

    :param errors: incidental error messages
    :param source: *dict* containing the value to be validated
    :param attr: the attribute associated with the value to be validated
    :param default: optional default value, overrides *required*
    :param required: specifies whether a value must be provided
    :param logger: optional logger
    :return: the validated value, or *None* if validation failed
    """
    # initialize the return variable
    result: str | None = None

    value: str = validate_str(errors=errors,
                              source=source,
                              attr=attr,
                              default=default,
                              required=required,
                              logger=logger)
    if value:
        email_pattern: str = r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"
        if re.match(email_pattern, value):
            result = value
        elif isinstance(errors, list):
            # 141: Invalid value {}
            errors.append(validate_format_error(141,
                                                value,
                                                f"@{attr}"))
    return result


def validate_pwd(errors: list[str] | None,
                 source: dict[str, Any],
                 attr: str,
                 required: bool = False,
                 logger: Logger = None) -> IntEnum | StrEnum | None:
    r"""
    Validate the password value associated with *attr* in *source*.

    If provided, this value must abide by the following rules:
      - length >= 8
      - at least 1 digit (in range [0-9])
      - at least 1 lower-case letter (in range [a-z])
      - at least 1 uppercase letter (in renge [A-Z])
      - at least 1 special character (#$%&"'()*+,-./:;<=>?@[\]^_`{|}~!)

    :param errors: incidental error messages
    :param source: *dict* containing the value to be validated
    :param attr: the attribute associated with the value to be validated
    :param required: specifies whether a value must be provided
    :param logger: optional logger
    :return: the validated value, or *None* if validation failed
    """
    # initialize the return variable
    result: str | None = None

    value: str = validate_str(errors=errors,
                              source=source,
                              attr=attr,
                              required=required,
                              logger=logger)
    if (value and len(value) >= 8 and
        str_find_char(source=value,
                      chars=string.digits) >= 0 and
        str_find_char(source=value,
                      chars=string.ascii_uppercase) >= 0 and
        str_find_char(source=value,
                      chars=string.ascii_lowercase) >= 0 and
        str_find_char(source=value,
                      chars=string.punctuation) >= 0):
        result = value
    elif not errors and isinstance(errors, list):
        # 237: Value does not meet the formation rules
        errors.append(validate_format_error(237,
                                            f"@{attr}"))
    return result


def validate_ints(errors: list[str] | None,
                  source: dict[str, Any],
                  attr: str,
                  sep: str = ",",
                  min_val: int = None,
                  max_val: int = None,
                  required: bool = False,
                  logger: Logger = None) -> list[int] | None:
    """
    Validate the list of *int* values associated with *attr* in *source*.

    If provided as a string, the elements therein must be valid string representations of *ints*,
    separated by *sep* (which defaults to a *comma*).

    :param errors: incidental error messages
    :param source: *dict* containing the list of values to be validated
    :param attr: the attribute associated with the list of values to be validated
    :param sep: the separator in the list of values
    :param min_val: the minimum value accepted
    :param max_val:  the maximum value accepted
    :param required: whether the list of values must be provided
    :param logger: optional logger
    :return: the list of validated values, *[]* if not required and no values found, or *None* if validation failed
    """
    # initialize the return variable
    result: list = []

    stat: str | None = None
    pos: int = attr.rfind(".") + 1
    suffix: str = attr[pos:]

    # obtain the values
    values: list = source.get(suffix)
    if values:
        if isinstance(values, str):
            values = str_as_list(source=values,
                                 sep=sep)
        if isinstance(values, list):
            if len(values) > 0:
                for inx, value in enumerate(values):
                    if (isinstance(value, str) and value.isdigit()) or \
                            (isinstance(value, int) and not isinstance(value, bool)):
                        result.append(int(value))
                        stat = validate_value(attr=f"@{attr}[{inx+1}]",
                                              val=int(value),
                                              min_val=min_val,
                                              max_val=max_val)
                    else:
                        # 152: Invalid value {}: must be type {}
                        stat = validate_format_error(152,
                                                     value,
                                                     "int",
                                                     f"@{attr}[{inx+1}]")
                    if stat:
                        break
            elif required:
                # 121: Required attribute
                stat = validate_format_error(121,
                                             f"@{attr}")
        else:
            # 152: Invalid value {}: must be type {}
            stat = validate_format_error(152,
                                         result,
                                         "list",
                                         f"@{attr}")
    elif required:
        # 121: Required attribute
        stat = validate_format_error(121,
                                     f"@{attr}")
    if stat:
        if logger:
            logger.error(msg=stat)
        if isinstance(errors, list):
            errors.append(stat)
        result = None

    return result


def validate_strs(errors: list[str] | None,
                  source: dict[str, Any],
                  attr: str,
                  sep: str = ",",
                  min_length: int = None,
                  max_length: int = None,
                  required: bool = False,
                  logger: Logger = None) -> list[str] | None:
    """
    Validate the list of *str* values associated with *attr* in *source*.

    If provided as a string, the elements therein must be separated by *sep* (which defaults to a *comma*).

    :param errors: incidental error messages
    :param source: *dict* containing the list of values to be validated
    :param attr: the attribute associated with the list of values to be validated
    :param sep: the separator in the list of values
    :param min_length: optional minimum length accepted
    :param max_length:  optional maximum length accepted
    :param required: whether the list of values must be provided
    :param logger: optional logger
    :return: the list of validated values, *[]* if not required and no values found, or *None* if validation failed
    """
    # initialize the return variable
    result: list | None = []

    stat: str | None = None
    pos: int = attr.rfind(".") + 1
    suffix: str = attr[pos:]

    # obtain the values
    values: list = source.get(suffix)
    if values:
        if isinstance(values, str):
            values = str_as_list(source=values,
                                 sep=sep)
        if isinstance(values, list):
            result = []
            if len(values) > 0:
                for inx, value in enumerate(values):
                    result.append(value)
                    if isinstance(value, str):
                        stat = validate_value(attr=f"@{attr}[{inx+1}]",
                                              val=value,
                                              min_val=min_length,
                                              max_val=max_length)
                    else:
                        # 152: Invalid value {}: must be type {}
                        stat = validate_format_error(152,
                                                     value,
                                                     "str",
                                                     f"@{attr}[{inx+1}]")
            elif required:
                # 121: Required attribute
                stat = validate_format_error(121,
                                             f"@{attr}")
        else:
            # 152: Invalid value {}: must be type {}
            stat = validate_format_error(152,
                                         result,
                                         "list",
                                         f"@{attr}")
    elif required:
        # 121: Required attribute
        stat = validate_format_error(121,
                                     f"@{attr}")
    if stat:
        if logger:
            logger.error(msg=stat)
        if isinstance(errors, list):
            errors.append(stat)
        result = None

    return result


def validate_format_error(error_id: int,
                          *args: Any,
                          **kwargs: dict) -> str:
    """
    Format and return the error message identified by *err_id* in the standard messages list.

    The message is built from the message element in the standard messages list, identified by *error_id*.
    The occurrences of *{}* in the element are sequentially replaced by the given *args*.
    When replacing, an instance of *args* is surrounded by single quotes if it contains no blankspaces.
    The element in *args* prefixed with *@*, if present, is appended to the end of the message.

    Optional custom language and prefix, replacing those defined respectively by the environment variables
    *VALIDATION_MSG_LANGUAGE* and *VALIDATION_MSG_PREFIX*, may be provided in *kwargs*, with the corresponding
    keys *msg_lang*, and *msg_prefix*.

    Suppose this function is invoked with:
      - *error_id*: 147 (defined as: Invalid value {}: length shorter than {})
      - *args*: 'my_value', 10, '@my_attr'
    The formatted error message will be (*<VMP>* is the validation message prefix):
      - <VMP>147: Invalid value 'my_value': length shorter than 10 @my_attr

    :param error_id: the identification of the message element
    :param args: optional non-keyworded arguments to format the error message with
    :param kwargs: optional keyworded arguments to define language and prefix
    :return: the formatted error message
    """
    # obtain definitions for prefix and language
    msg_prefix: str = kwargs.get("msg_prefix") if "msg_prefix" in kwargs else VALIDATION_MSG_PREFIX
    msg_lang: MsgLang = validate_enum(errors=None,
                                      source=kwargs or {},
                                      attr="msg_lang",
                                      enum_class=MsgLang,
                                      default=VALIDATION_MSG_LANGUAGE)
    # retrieve the messages list
    match msg_lang:
        case MsgLang.EN:
            from .validation_msgs import _ERR_MSGS_EN
            err_msgs = _ERR_MSGS_EN
        case MsgLang.PT:
            from .validation_msgs import _ERR_MSGS_PT
            err_msgs = _ERR_MSGS_PT
        case _:
            err_msgs = {}

    # initialize the return variable
    result: str = ""
    if msg_prefix:
        result += msg_prefix + str(error_id) + ": "
    result += err_msgs.get(error_id) or ""

    # apply the provided arguments
    for arg in args:
        if arg is None:
            pos1: int = result.find(": {}")
            pos2: int = result.find(" {}")
            if pos1 < 0 or pos2 < pos1:
                result = result.replace(" {}", "", 1)
            else:
                result = result.replace(": {}", "", 1)
        elif not result or (isinstance(arg, str) and arg.startswith("@")):
            result += " " + arg
        elif isinstance(arg, str) and arg.find(" ") > 0:
            result = result.replace("{}", arg, 1)
        else:
            result = result.replace("{}", f"'{arg}'", 1)

    return result


def validate_format_errors(errors: list[str],
                           **kwargs: dict) -> list[dict[str, str]]:
    """
    Build and return a list of *dicts* from the list of errors in *errors*.

    Each element in *errors* is encoded as a *dict*.
    This list is tipically used in a returning *JSON* string.

    Optional custom language and prefix, replacing those defined respectively by the environment variables
    *VALIDATION_MSG_LANGUAGE* and *VALIDATION_MSG_PREFIX*, may be provided in *kwargs*, with the corresponding
    keys *msg_lang*, and *msg_prefix*.

    Suppose *errors* contains (*<VMP>* is the validation message prefix):
      - <VMP>147: Invalid value 'my_value': length shorter than 10 @my_attr
    The returned list will contain the *dict*:
      - {
      -   "attribute": "my_attr",
      -   "code": <VMP>147,
      -   "description": Invalid value 'my_value': length shorter than 10
      - }

    :param errors: the list of errors to build the list of *dicts* with
    :param kwargs: optional keyworded arguments to define formatting language and prefix
    :return: the built list of *dicts*
    """
    # obtain definitions for prefix and language
    msg_prefix: str = kwargs.get("msg_prefix") if "msg_prefix" in kwargs else VALIDATION_MSG_PREFIX
    msg_lang: MsgLang = validate_enum(errors=None,
                                      source=kwargs or {},
                                      attr="msg_lang",
                                      enum_class=MsgLang,
                                      default=VALIDATION_MSG_LANGUAGE)
    # initialize the return variable
    result: list[dict[str, str]] = []

    # extract error code, description, and attribute from text
    for error in errors:

        # locate the last indicator for the attribute
        pos = error.rfind("@")

        # is there a whitespace in the attribute's name ?
        if pos > 0 and str_find_whitespace(error[pos:]) > 0:
            # yes, disregard the attribute
            pos = -1

        # was the attribute's name found ?
        if pos == -1:
            # no
            out_error: dict[str, str] = {}
            desc: str = error
        else:
            # yes
            term: str = "attribute" if msg_lang == MsgLang.EN else "atributo"
            out_error: dict[str, str] = {term: error[pos + 1:]}
            desc: str = error[:pos - 1]

        # does the text contain an error code ?
        if msg_prefix and desc.startswith(msg_prefix):
            # yes
            term: str = "code" if msg_lang == MsgLang.EN else "codigo"
            pos: int = desc.find(":")
            out_error[term] = desc[0:pos]
            desc = desc[pos+2:]

        term: str = "description" if msg_lang == MsgLang.EN else "descricao"
        out_error[term] = desc
        result.append(out_error)

    return result


def validate_unformat_errors(errors: list[dict[str, str] | str],
                             **kwargs: dict) -> list[str]:
    """
    Extract and return the list of errors used to build the list of dicts *errors*.

    Optional custom language, replacing the one defined by the environment variable
    *VALIDATION_MSG_LANGUAGE*, may be provided in *kwargs*, with the key *msg_lang*.

    :param errors: the list of dicts to extract the errors from
    :param kwargs: optional keyworded arguments to define formatting language
    :return: the built list of errors
    """
    # obtain definitions for language and prefix
    msg_lang: MsgLang = validate_enum(errors=None,
                                      source=kwargs or {},
                                      attr="msg_lang",
                                      enum_class=MsgLang,
                                      default=VALIDATION_MSG_LANGUAGE)
    # initialize the return variable
    result: list[str] = []

    # define the dictionary keys
    name: str = "code" if msg_lang == MsgLang.EN else "codigo"
    desc: str = "description" if msg_lang == MsgLang.EN else "descricao"

    # traverse the list of dicts
    for error in errors:
        if isinstance(error, dict):
            desc: str = str_sanitize(source=error.get(desc) or "''")
            result.append(f"{error.get(name)}: {desc}")
        else:
            result.append(error)

    return result
