"""Utility functions for the SpectrumX SDK."""

from __future__ import annotations

import logging
import os
import random
import re
import string
from pathlib import Path
from typing import TYPE_CHECKING
from typing import NoReturn
from typing import TypeVar
from typing import cast

from blake3 import blake3 as Blake3  # noqa: N812
from loguru import logger as log
from tqdm import auto as auto_tqdm
from tqdm import tqdm

if TYPE_CHECKING:
    from collections.abc import Iterable

logger = logging.getLogger(__name__)


def validate_file_permission_string(permissions: str) -> None:
    """Make sure a unix-like permissions string is valid."""
    perm_flags_len = 9
    assert len(permissions) == perm_flags_len, (
        "Invalid permissions string. Expected 9 characters."
    )
    valid_chars = {"r", "w", "x", "-"}
    assert set(permissions).issubset(
        valid_chars,
    ), "Invalid permission characters: use 'r', 'w', 'x', or '-'"
    assert all(permissions[idx * 3] in {"r", "-"} for idx in range(3)), (
        "Invalid read permissions"
    )
    assert all(permissions[idx * 3 + 1] in {"w", "-"} for idx in range(3)), (
        "Invalid write permissions"
    )
    assert all(permissions[idx * 3 + 2] in {"x", "-"} for idx in range(3)), (
        "Invalid execute permissions"
    )


def log_user(msg: str, depth: int = 1) -> None:
    """Alias to log_user_info. Logs a message visible to SDK users."""
    log_user_info(msg, depth=depth + 1)


def log_user_info(msg: str, depth: int = 1) -> None:
    """Logs an informational message to the user."""
    log.opt(depth=depth).info(msg)
    logger.info(msg)


def log_user_warning(msg: str, depth: int = 1) -> None:
    """Logs a warning message to the user."""
    log.opt(depth=depth).warning(msg)
    logger.warning(msg)


def log_user_error(msg: str, depth: int = 1) -> None:
    """Logs an error message to the user."""
    log.opt(depth=depth).error(msg)
    logger.error(msg)


def into_human_bool(value: str | int | bool) -> bool:  # noqa: FBT001
    """Converts a string to a boolean value, defaulting to False when invalid."""
    if isinstance(value, bool):
        return value
    if isinstance(value, int):
        return bool(value)
    if isinstance(value, str):
        return value.lower() in {"t", "true", "1", "y", "yes", "on", "enabled"}
    return False


def is_running_in_notebook() -> bool:
    """Check if the current environment is a Jupyter notebook."""
    try:
        from IPython import (  # pyright: ignore[reportMissingModuleSource, reportMissingImports]  # noqa: PLC0415 # pyrefly: ignore[missing-import]
            get_ipython,  # pyright: ignore[reportPrivateImportUsage]
        )

        if "ZMQInteractiveShell" in str(get_ipython()):
            running_in_notebook = True
        else:
            running_in_notebook = False
    except ImportError:
        running_in_notebook = False
    return running_in_notebook


def is_test_env() -> bool:
    """Returns whether the current environment is a test environment.

    Useful for:
        + Deciding if SSL verification can be skipped;
        + Whether debug logs should be enabled on error;
    """
    env_var = os.getenv("PYTEST_CURRENT_TEST", default=None)
    return env_var is not None


def get_random_line(length: int, *, include_punctuation: bool = True) -> str:
    """Generates a random string of a given length."""
    char_choices = (
        [*string.ascii_letters, *string.digits, *string.punctuation]
        if include_punctuation
        else [*string.ascii_letters, *string.digits]
    )
    return "".join(
        [
            random.choice(char_choices)  # noqa: S311
            for _ in range(length)
        ]
    )


def sum_blake3(file_path: Path) -> str:
    """Calculates the BLAKE3 checksum of a file."""
    checksum = Blake3()
    with file_path.open("rb") as file:
        for chunk in iter(lambda: file.read(4096), b""):
            checksum.update(chunk)
    return checksum.hexdigest()


def clean_local_path(local_path: Path) -> Path:
    """Hack to remove what looks like an SDS user ID from local path.

    This is non-critical and will be unnecessary after some gateway changes...
    """
    # match email-like strings at the second level
    n_level = 2 if local_path.is_absolute() else 1
    email_like_pattern = r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}"
    local_path_2nd_level: str = local_path.parts[n_level]
    is_dirty = bool(re.match(email_like_pattern, string=local_path_2nd_level))
    if not is_dirty:
        return local_path
    # remove the top 2 levels
    return Path(*local_path.parts[n_level + 1 :])


T = TypeVar("T")


def get_prog_bar(
    iterable: Iterable[T] | None = None, *args, **kwargs
) -> tqdm[T] | tqdm[NoReturn]:  # pyrefly: ignore[bad-specialization]
    """SDS standard progress bar."""
    default_options = {
        "unit": "files",
        "unit_scale": True,
        # width in the terminal is in characters; in jupyter it's in pixels
        "ncols": 800 if is_running_in_notebook() else 120,
        # adjust colors for visibility:
        #   terminals usually have a dark bg; jupyter outputs usually have a light bg
        "colour": "green" if is_running_in_notebook() else "yellow",
    }
    kwargs = {**default_options, **kwargs}

    return cast(
        "tqdm[T]",  # pyrefly: ignore[bad-specialization]
        auto_tqdm.tqdm(iterable, *args, **kwargs),
    )
