# SPDX-FileCopyrightText: 2025 Max Mehl <https://mehl.mx>
#
# SPDX-License-Identifier: Apache-2.0

"""Utility functions for Castmail2List application"""

import logging
import os
import subprocess
import sys
import uuid
from datetime import datetime, timedelta, timezone
from email.utils import formataddr
from pathlib import Path

import email_validator  # dependency for WTForms email validator
from flask import Flask, flash
from flask_babel import _
from imap_tools import EmailAddress, MailBox, MailboxLoginError
from imap_tools.message import MailMessage
from platformdirs import user_config_path
from sqlalchemy import func

from . import __version__
from .models import EmailIn, EmailOut, Logs, MailingList, Subscriber, db


def compile_scss(compiler: str, scss_input: str, css_output: str) -> None:
    """Compile SCSS files to CSS using an external compiler"""
    try:
        logging.info("Compiling %s to %s", scss_input, css_output)
        subprocess.run([compiler, scss_input, css_output], check=True)
    except subprocess.CalledProcessError as e:
        logging.critical("Error compiling %s: %s", scss_input, e)
        sys.exit(1)
    except FileNotFoundError as e:
        logging.critical(
            "Sass compiler not found. Please ensure '%s' is installed: %s", compiler, e
        )
        sys.exit(1)


def compile_scss_on_startup(scss_files: list[tuple[str, str]]) -> list[tuple[str, str]]:
    """Compile SCSS to CSS on application startup.

    Args:
        scss_files (list[tuple[str, str]]): List of tuples with relative paths
    Return:
        list: List of compiled (input, output) absolute file paths
    """
    curpath = Path(__file__).parent.resolve()
    compiled_files: list[tuple[str, str]] = []
    for scss_input, css_output in scss_files:
        scss_input_abs = str(curpath / Path(scss_input))
        css_output_abs = str(curpath / Path(css_output))
        compile_scss("sass", scss_input=scss_input_abs, css_output=css_output_abs)
        compiled_files.append((scss_input_abs, css_output_abs))
    return compiled_files


def flash_form_errors(form):
    """Flash all errors from a Flask-WTF form"""
    for field, errors in form.errors.items():
        for error in errors:
            flash(f"Error in {getattr(form, field).label.text}: {error}", "error")


def get_version_info(debug: bool = False) -> str:
    """
    Get the current version information of the application. If in debug mode, include git commit
    hash.

    Example: "1.2.3 (a1b2c3d)" in debug mode, "1.2.3" otherwise.

    Args:
        debug (bool): Whether to include git commit hash information
    Returns:
        str: The version information string
    """
    if not debug:
        return __version__
    # Get short git commit hash if available
    try:
        commit = subprocess.check_output(["git", "rev-parse", "--short", "HEAD"]).decode().strip()
    except Exception:  # pylint: disable=broad-exception-caught
        logging.debug("Failed to get git commit hash.", exc_info=True)
        commit = "unknown commit"

    return f"{__version__} ({commit})"


def normalize_email_list(input_str: str) -> str:
    """Normalize a string of emails into a comma-separated list"""
    # Accepts either comma or newline separated, returns comma-separated
    if not input_str:
        return ""
    # Replace newlines with commas, then split
    emails = [email.strip() for email in input_str.replace("\n", ",").split(",") if email.strip()]
    return ", ".join(emails)


def list_to_string(listobj: list[str]) -> str:
    """Convert a list to a comma-separated string"""
    if isinstance(listobj, list):
        return ", ".join(listobj)
    logging.warning("Input is not a list: %s", listobj)
    return ""


def string_to_list(input_str: str, lower: bool = False) -> list[str]:
    """
    Normalize a string of strings into a list.

    Args:
        input_str (str): Input string with comma or newline separated values
        lower (bool): Whether to convert all strings to lowercase
    Returns:
        list[str]: List of normalized strings
    """
    # Accepts either comma or newline separated, returns list of strings
    if not input_str:
        return []
    # Replace newlines with commas, then split
    strings = [
        string.strip() for string in input_str.replace("\n", ",").split(",") if string.strip()
    ]
    # Optionally convert to lowercase
    if lower:
        strings = [s.lower() for s in strings]
    return strings


def create_bounce_address(ml_address: str, recipient: str) -> str:
    """
    Construct the individualized Envelope From address for bounce handling.

    For the list address `list1@list.example.com` and the recipient `jane.doe@gmail.com`,
    the return will be `list1+bounces--jane.doe=gmail.com@list.example.com`

    Args:
        recipient (str): The recipient email address
    Returns:
        str: The constructed Envelope From address
    """
    local_part, domain_part = ml_address.split("@", 1)
    sanitized_recipient = recipient.replace("@", "=").replace("+", "---plus---")
    return f"{local_part}+bounces--{sanitized_recipient}@{domain_part}"


def parse_bounce_address(bounce_address: str) -> str | None:
    """
    Parse the recipient email from a bounce address.

    For the bounce address `list1+bounces--jane.doe=gmail.com@list.example.com`, the return will be
    `jane.doe@gmail.com`

    Args:
        bounce_address (str): The bounce email address

    Returns:
        (str | None): The parsed recipient email address, or None if parsing fails
    """
    try:
        local_part, _ = bounce_address.split("@", 1)
        if "+bounces--" not in local_part:
            logging.debug("No bounce marker in address: %s", bounce_address)
            return None
        _, sanitized_recipient = local_part.split("+bounces--", 1)
        recipient = sanitized_recipient.replace("=", "@").replace("---plus---", "+")
        return recipient
    except ValueError:
        logging.warning("Failed to parse bounce address: %s", bounce_address)
        return None


def generate_via_from_header(
    from_values: EmailAddress | None, ml_address: str, ml_display: str
) -> str:
    """Generate 'Sender Name via List Name <list@address>' format for From header.

    Args:
        from_values (EmailAddress | None): The original From header values
        ml_address (str): The mailing list email address
        ml_display (str): The display name of the mailing list

    Returns:
        str: Formatted From header string with proper encoding
    """
    if not from_values:
        logging.error("No valid From header, cannot generate via header")
        return ""
    display_name = f"{from_values.name or from_values.email} via {ml_display}"
    return formataddr((display_name, ml_address))


def reply_to_from_email_and_name(email: str, name: str | None = None) -> str:
    """
    Generate 'Sender Name <email>' format for Reply-To header from the EmailAddress object generated
    by imap-tools.

    Args:
        email (str): The email address
        name (str | None): The display name
    Returns:
        str: Formatted Reply-To header string with proper encoding
    """
    if name:
        return formataddr((name, email))
    return email


def is_email_a_list(email: str) -> MailingList | None:
    """
    Check if the given email address is the address of one of the configured active or inactive
    mailing lists.

    To avoid case sensitivity issues, the check is done in a case-insensitive manner.

    It also removes any +suffix before checking.

    Args:
        email (str): The email address to check
    Returns:
        The MailingList object if the email is a list address, None otherwise
    """
    email = remove_plus_suffix(email)
    if ml := MailingList.query.filter(func.lower(MailingList.address) == func.lower(email)).first():
        return ml
    return None


def get_list_by_id(list_id: str) -> MailingList | None:
    """
    Get a mailing list by its ID.

    Args:
        list_id (str): The ID of the mailing list
    Returns:
        MailingList | None: The MailingList object if found, None otherwise
    """
    return MailingList.query.filter_by(id=list_id).first()


def get_list_recipients_recursive(
    list_id: str, only_direct: bool = False, only_indirect: bool = False
) -> dict[str, dict]:
    """
    Get all real recipients for a mailing list with direct and indirect breakdown. The result is a
    mapping of recipient email to their details and source lists. Direct subscribers are marked with
    source ["direct"]. Indirect recipients have source list IDs they are subscribed through.

    Notes:
    * Recipients are not the same as subscribers. A subscriber is always direct and may be a list. A
      recipient is always a real person and may also be coming from indirect subscription via nested
      lists.
    * It may happen that a recipient appears both as direct and indirect recipient.

    Args:
        list_id (str): The ID of the mailing list
        only_direct (bool): If True, return only direct recipients
        only_indirect (bool): If True, return only indirect recipients

    Returns:
        dict: Mapping of recipient email to their details and source lists. If the mailing list is
            not found, returns an empty dictionary.
    """
    visited_list_ids = set()
    recipients_dict: dict[str, dict] = {}

    ml = get_list_by_id(list_id)
    if not ml:
        logging.warning("Mailing list with ID %s not found.", list_id)
        return recipients_dict

    def _collect_recipients(list_obj: MailingList, is_direct: bool = False):
        """Recursively collect subscribers from the given mailing list and nested lists"""
        if list_obj.id in visited_list_ids:  # list already visited, avoid recursion
            return
        visited_list_ids.add(list_obj.id)  # Mark this list as visited

        # Exclude deleted lists
        if list_obj.deleted:
            return

        # Get direct subscribers
        direct_subs: list[Subscriber] = Subscriber.query.filter_by(list_id=list_obj.id).all()
        for rec in direct_subs:
            # Add subscriber if not already added
            if rec.email not in recipients_dict:
                recipients_dict[rec.email] = {
                    "id": rec.id,
                    "name": rec.name,
                    "email": rec.email,
                    "source": ["direct"] if is_direct else [list_obj.id],
                }
            else:
                # Update source list
                if is_direct:
                    if "direct" not in recipients_dict[rec.email]["source"]:
                        recipients_dict[rec.email]["source"].append("direct")
                else:
                    if list_obj.id not in recipients_dict[rec.email]["source"]:
                        recipients_dict[rec.email]["source"].append(list_obj.id)

        # Iterate over direct recipients. If any is a list, recurse into it
        for rec in direct_subs:
            if nested_list := is_email_a_list(rec.email):
                # Only recurse if the nested list hasn't been visited yet
                if nested_list.id not in visited_list_ids:
                    _collect_recipients(nested_list, is_direct=False)

    # Start collecting from the given mailing list
    _collect_recipients(ml, is_direct=True)

    # Remove any recipient whose email is a list address (do not send to lists themselves)
    for email in list(recipients_dict.keys()):
        if is_email_a_list(email):
            del recipients_dict[email]

    # Filter based on only_direct / only_indirect flags
    if only_direct and only_indirect:
        # If both are requested, return empty dict (no email can be both)
        return {}
    if only_direct:
        recipients_dict = {
            email: details
            for email, details in recipients_dict.items()
            if "direct" in details.get("source", [])
        }
    elif only_indirect:
        recipients_dict = {
            email: details
            for email, details in recipients_dict.items()
            if "direct" not in details.get("source", [])
        }

    # Sort by email
    recipients_dict = dict(sorted(recipients_dict.items(), key=lambda item: item[0]))

    logging.debug(
        "Found %d unique, non-list recipients (after optional filters) with details for the list "
        "<%s>: %s",
        len(recipients_dict),
        ml.address,
        ", ".join(recipients_dict.keys()),
    )

    return recipients_dict


def get_list_subscribers(list_id: str, exclude_lists: bool = False) -> dict[str, dict]:
    """
    Get all direct subscribers for a mailing list. The result is a mapping of subscriber email to
    their details.

    Notes:

    * A subscriber is always a direct subscription to the mailing list, and may be either a real
      person or a list. This is different from recipients, which are always real people and may be
      direct or indirect by being subscribed via nested lists.

    Args:
        list_id (str): The ID of the mailing list

        exclude_lists (bool): Whether to exclude subscribers that are mailing lists themselves
    Returns:
        dict: Mapping of subscriber email to their details. If the mailing list is not found,
            returns an empty dictionary.
    """
    subscribers_dict: dict[str, dict] = {}

    ml = get_list_by_id(list_id)
    if not ml:
        logging.warning("Mailing list with ID %s not found.", list_id)
        return subscribers_dict

    # Get direct subscribers
    direct_subs: list[Subscriber] = (
        Subscriber.query.order_by(Subscriber.email).filter_by(list_id=ml.id).all()
    )
    for sub in direct_subs:
        # Skip if subscriber is a list and include_lists is False
        if exclude_lists and is_email_a_list(sub.email):
            continue
        subscribers_dict[sub.email] = {
            "id": sub.id,
            "name": sub.name,
            "email": sub.email,
            "comment": sub.comment,
            "subscriber_type": sub.subscriber_type,
            "list_id": sub.list_id,
        }

    logging.debug(
        "Found %d direct subscribers (after optional filters) with details for the list <%s>: %s",
        len(subscribers_dict),
        ml.address,
        ", ".join(subscribers_dict.keys()),
    )

    return subscribers_dict


def get_all_subscribers() -> dict[str, dict[str, list[MailingList] | int]]:
    """
    Get all subscribers from the database, the lists they are subscribed to, and the number of
    bounces. As subscribers may be subscribed to multiple lists, the result is a mapping of email
    address, not the individual DB record.

    Returns:
        dict: A mapping of email addresses to the lists they are subscribed to and their
        bounce count
    """
    subscriber_map: dict[str, dict] = {}

    all_lists: list[MailingList] = MailingList.query.all()
    for ml in all_lists:
        subscribers: list[Subscriber] = Subscriber.query.filter_by(list_id=ml.id).all()
        for sub in subscribers:
            if sub.email not in subscriber_map:
                subscriber_map[sub.email] = {"lists": [], "bounces": 0}
            subscriber_map[sub.email]["lists"].append(ml)
            subscriber_map[sub.email]["bounces"] += sub.bounces

    # sort subscriber_map by email
    subscriber_map = dict(sorted(subscriber_map.items(), key=lambda item: item[0]))

    return subscriber_map


def get_plus_suffix(email: str) -> str | None:
    """
    Extract the +suffix from an email address, if present.

    Args:
        email (str): The email address to extract the suffix from
    Returns:
        str | None: The suffix (without the +), or None if no suffix is present
    """
    local_part, _ = email.split("@", 1)
    if "+" in local_part:
        suffix = local_part.split("+", 1)[1]
        return suffix
    return None


def remove_plus_suffix(email: str) -> str:
    """
    Remove the +suffix from an email address, if present.

    Args:
        email (str): The email address to remove the suffix from
    Returns:
        str: The email address without the +suffix
    """
    local_part, domain_part = email.split("@", 1)
    if "+" in local_part:
        local_part = local_part.split("+", 1)[0]
    return f"{local_part}@{domain_part}"


def is_expanded_address_the_mailing_list(to_address: str, list_address: str) -> bool:
    """
    Check if the given (expanded) To address corresponds to the mailing list address,
    considering possible +suffixes and casing.

    Args:
        to_address (str): The (expanded) To email address
        list_address (str): The mailing list address to compare against
    Returns:
        bool: True if the address matches the mailing list address, False otherwise
    """
    to_local_part, to_domain_part = to_address.split("@", 1)
    list_local_part, list_domain_part = list_address.split("@", 1)

    # Check domain parts (case-insensitive)
    if to_domain_part.lower() != list_domain_part.lower():
        return False

    # Check local parts (case-insensitive, ignoring +suffix)
    to_local_part_no_suffix = to_local_part.split("+", 1)[0].lower()

    return to_local_part_no_suffix == list_local_part.lower()


def run_only_once(app: Flask):
    """Ensure that something is only run once if Flask is run in Debug mode. Check if Flask is run
    in Debug mode and what the value of env variable WERKZEUG_RUN_MAIN is"""
    logging.debug("FLASK_DEBUG=%s, WERKZEUG_RUN_MAIN=%s", app.debug, os.getenv("WERKZEUG_RUN_MAIN"))

    if not app.debug:
        return True
    if app.debug and os.getenv("WERKZEUG_RUN_MAIN") == "true":
        return True
    return False


def check_email_account_works(
    imap_host: str, imap_port: int, imap_user: str, imap_password: str
) -> bool:
    """
    Check if an email account exists on the IMAP server.

    Args:
        app (Flask): The Flask application instance for accessing configuration
        email (str): The email address to check
    Returns:
        bool: True if the email account exists, False otherwise
    """
    try:
        with MailBox(imap_host, imap_port).login(imap_user, imap_password):
            logging.debug("Successfully logged in to IMAP server %s as %s", imap_host, imap_user)
            return True
    except MailboxLoginError:
        logging.warning("Failed to log in to IMAP server %s as %s", imap_host, imap_user)
        return False
    except Exception as e:  # pylint: disable=broad-exception-caught
        logging.error(
            "Error while checking email account on IMAP server %s as %s: %s",
            imap_host,
            imap_user,
            e,
        )
        return False


def split_email_address(email: str) -> tuple[str, str]:
    """
    Split an email address into local part and domain part.

    Args:
        email (str): The email address to split
    Returns:
        tuple[str, str]: A tuple containing the local part and domain part
    """
    local_part, domain_part = email.split("@", 1)
    return local_part, domain_part


def create_email_account(host_type: str, email: str, password: str) -> bool:
    """
    Create an email account on the server.

    Args:
        host_type (str): The type of hosting environment (e.g., 'uberspace7', 'uberspace8')
        email (str): The email address to create
        password (str): The password for the new email account
    Returns:
        bool: True if the email account was created successfully, False otherwise
    """
    logging.info("Creating email account %s on host type %s", email, host_type)
    try:
        if host_type == "uberspace7":
            cmd = [
                "uberspace",
                "mail",
                "user",
                "add",
                "-p",
                password,
                split_email_address(email)[0],
            ]
        elif host_type == "uberspace8":
            cmd = ["uberspace", "mail", "address", "add", "--password", password, email]
        else:
            logging.error("Unsupported host type for email account creation: %s", host_type)
            return False

        subprocess.run(cmd, check=True)
        logging.info("Successfully created email account: %s", email)
        return True
    except subprocess.CalledProcessError as e:
        logging.error("Failed to create email account %s: %s", email, e)
        return False
    except FileNotFoundError as e:
        logging.error(
            "Uberspace command not found. Ensure application actually runs on Uberspace host: %s",
            e,
        )
        return False


def check_recommended_list_setting(ml: MailingList) -> list[tuple[str, str]]:
    """
    Check if the mailing list has recommended security settings.

    Args:
        ml (MailingList): The mailing list to check
    Returns:
        list[tuple[str, str]]: A list of warnings about missing recommended settings
    """
    findings = []

    if ml.mode == "broadcast":
        if not ml.allowed_senders and not ml.sender_auth:
            findings.append(
                (
                    _(
                        "In Broadcast mode, it is recommended to set Allowed Senders and/or "
                        "Sender Authentication Passwords!"
                    ),
                    "warning",
                )
            )

    return findings


def get_app_bin_dir() -> Path:
    """
    Get the directory where this app's executable resides in the current Python environment.
    """
    return Path(sys.executable).parent


def get_user_config_path(name: str = "castmail2list", file: str = "") -> str:
    """
    Get the user configuration directory for the application.

    Args:
        app_name (str): The name of the application
        file (str): Optional filename to append to the config directory
    Returns:
        str: The path to the user configuration directory
    """
    config_path = Path(user_config_path(appname=name, ensure_exists=True))
    if file:
        config_path = config_path / file
    return str(config_path)


def get_all_incoming_messages(only: str = "", days: int = 0) -> list[EmailIn]:
    """
    Get all incoming messages from the database. With options to filter for bounce messages and by
    date.

    Args:
        only (str): Filter the messages
            * If "ok", return only successful
            * If "bounces", return only bounce messages
            * If "failures", return only failure messages (except bounces)
            * If empty, return all messages
        days (int): Only return messages from the last given number of days. If 0, return all

    Returns:
        list[Message]: A list of all requested messages, descending by received date
    """
    if only not in ("", "bounces", "failures", "ok"):
        logging.critical("Invalid 'only' parameter for get_all_messages: %s", only)
        raise ValueError(f"Invalid 'only' parameter: {only}")
    all_messages: list[EmailIn] = EmailIn.query.order_by(EmailIn.received_at.desc()).all()
    if only == "bounces":
        all_messages = [msg for msg in all_messages if msg.status == "bounce-msg"]
    if only == "failures":
        all_messages = [msg for msg in all_messages if msg.status not in ("ok", "bounce-msg")]
    if only == "ok":
        all_messages = [msg for msg in all_messages if msg.status == "ok"]
    if days > 0:
        cutoff_date = datetime.now() - timedelta(days=days)
        all_messages = [msg for msg in all_messages if msg.received_at >= cutoff_date]
    return all_messages


def get_all_outgoing_messages(days: int = 0) -> list[EmailOut]:
    """
    Get all outgoing messages from the database. With option to filter by date.

    Args:
        days (int): Only return messages from the last given number of days. If 0, return all
    Returns:
        list[EmailOut]: A list of all requested outgoing messages, descending by sent date
    """
    all_messages: list[EmailOut] = EmailOut.query.order_by(EmailOut.sent_at.desc()).all()
    if days > 0:
        cutoff_date = datetime.now() - timedelta(days=days)
        all_messages = [msg for msg in all_messages if msg.sent_at >= cutoff_date]
    return all_messages


def get_all_messages_id_from_raw_email(raw_email: str) -> list[str]:
    """
    Extract the (Original-)Message-IDs from a raw email string.

    Args:
        raw_email (str): The raw email content as a string
    Returns:
        list[str]: A list of Message-ID/Original-Message-ID found in the email
    """
    message_ids = []
    for line in raw_email.splitlines():
        if line.lower().startswith("message-id:") or line.lower().startswith(
            "original-message-id:"
        ):
            _, msg_id = line.split(":", 1)
            message_ids.append(msg_id.strip().strip("<>"))
    return message_ids


def get_message_id_in_db(
    message_ids: list[str], only: str = "", list_id: str = ""
) -> list[EmailIn | EmailOut]:
    """
    Check if any of the given Message-IDs exist in the database as either incoming or outgoing
    messages. Returns all found messages.

    Can be filtered to:
    * only check incoming or outgoing messages or both
    * connected list ID

    Args:
        message_ids (list[str]): A list of Message-IDs to check
        only (str): "in" to check only incoming messages, "out" for outgoing. Empty for both
        list_id (str): If given, only check messages connected to this mailing list ID

    Returns:
        list: A list of found EmailIn or EmailOut objects matching any of the given Message-IDs.
        Returns an empty list if none found.
    """
    if only not in ("", "in", "out"):
        logging.critical("Invalid 'filter' parameter for get_message_id_in_db: %s", only)
        raise ValueError(f"Invalid 'filter' parameter: {only}")

    found_messages: list[EmailIn | EmailOut] = []

    if only in ("", "in"):
        for msg_id in message_ids:
            msg_in: list[EmailIn] = EmailIn.query.filter_by(message_id=msg_id).all()
            found_messages.extend(msg_in)

    if only in ("", "out"):
        for msg_id in message_ids:
            msg_out: list[EmailOut] = EmailOut.query.filter_by(message_id=msg_id).all()
            found_messages.extend(msg_out)

    if list_id:
        found_messages = [msg for msg in found_messages if getattr(msg, "list_id", "") == list_id]
    if len(found_messages) > 1:
        logging.warning(
            "Multiple messages found in DB for Message-IDs %s and list ID %s. This may indicate a "
            "serious DB issue as there is a composite primary key on (message_id, list_id).",
            message_ids,
            list_id,
        )

    return found_messages


def get_message_id_from_incoming(msg: MailMessage) -> str:
    """
    Extract the Message-ID from an incoming MailMessage object.

    Args:
        msg (MailMessage): The incoming email message
    Returns:
        str: The Message-ID of the email, without < > brackets; if not present, a new UUID is
        generated
    """
    return next(iter(msg.headers.get("message-id", ())), str(uuid.uuid4())).strip("<>")


def create_log_entry(  # pylint: disable=too-many-arguments, too-many-positional-arguments
    level: str,
    event: str,
    message: str,
    details: dict | None = None,
    list_id: str | None = None,
) -> Logs:
    """
    Create and persist a log entry in the database.

    Args:
        level (str): Log level (e.g., 'info', 'warning', 'error')
        event (str): Event type (e.g., 'email_sent', 'bounce_received')
        message (str): Log message text
        details (dict | None): Optional JSON-serializable details dictionary
        list_id (str | None): Optional mailing list ID this log relates to

    Returns:
        Logs: The created and persisted log entry
    """

    log_entry = Logs(
        level=level.lower(),
        event=event.lower(),
        message=message,
        details=details or {},
        list_id=list_id,
        timestamp=datetime.now(timezone.utc),
    )

    db.session.add(log_entry)
    db.session.commit()

    return log_entry


def get_log_entries(exact: bool = False, days: int = 0, **kwargs) -> list[Logs]:
    """
    Retrieve log entries from the database based on provided filters.

    Args:
        exact (bool): If True, use exact matching; if False, use partial matching
        days (int): Only return log entries from the last given number of days. If 0, return all
        **kwargs: Filter criteria for querying logs (e.g., level='error', list_id=1)
    Returns:
        list[Logs]: A list of log entries matching the filter criteria
    """
    query = Logs.query

    for key, value in kwargs.items():
        column = getattr(Logs, key, None)
        if column is not None:
            if exact:
                query = query.filter(column == value)
            else:
                query = query.filter(column.ilike(f"%{value}%"))
        else:
            logging.warning("Invalid filter key for get_log_entries: %s", key)

    log_entries: list[Logs] = query.order_by(Logs.timestamp.desc()).all()
    if days > 0:
        cutoff_date = datetime.now() - timedelta(days=days)
        log_entries = [log for log in log_entries if log.timestamp >= cutoff_date]
    return log_entries


def validate_email(email: str, allow_smtputf8: bool = True) -> bool:
    """
    Validate the format of an email address.

    Args:
        email (str): The email address to validate
        allow_smtputf8 (bool): Whether to allow SMTPUTF8 addresses
    Returns:
        bool: True if the email format is valid, False otherwise
    """
    try:
        email_validator.validate_email(email, allow_smtputf8=allow_smtputf8)
        return True
    except email_validator.EmailNotValidError as e:
        logging.debug("Email validation failed for %s: %s", email, e)
        return False
