"""
standalone_utils.py
===================

Standalone, dependency-free extractions of selected ``abstract_utilities``
functions.

Every public function in this module was pulled out of the ``abstract_utilities``
package and rewritten / inlined so that it depends **only on the Python standard
library**.  There are no ``from abstract_utilities ...`` imports and no
``from .imports import *`` style imports anywhere in this file, so it can be
dropped into any project and used on its own.

Functions exported here (with their original home in the package):

    derive_media_type     <- type_utils/mime_types.py
    eatAll                <- string_utils/eat_utils.py
    get_any_value         <- json_utils/json_utils.py
    get_caller_dir        <- class_utils/caller_utils.py
    get_dirlist           <- path_utils/path_utils.py
    get_env_value         <- env_utils/* (re-implemented, no abstractEnv class)
    get_file_parts        <- path_utils/path_utils.py
    get_files             <- path_utils/path_utils.py
    get_files_and_dirs    <- file_utils/.../find_collect.py (local-only rewrite)
    get_home_dir          <- path_utils/path_utils.py
    get_inputs            <- class_utils/abstract_classes.py
    get_logFile           <- log_utils/log_file.py
    is_dir                <- file_utils/.../classes.py (local-only)
    is_file               <- file_utils/.../classes.py (local-only)
    is_number             <- type_utils/is_type.py
    join_path             <- path_utils/path_utils.py
    lazy_import           <- import_utils/.../lazy_utils.py
    make_list             <- list_utils/list_utils.py
    makedirs              <- directory_utils/directory_utils.py
    read_from_file        <- read_write_utils/read_write_utils.py (local-only)
    safe_dump_to_file     <- json_utils/json_utils.py
    safe_dump_to_json     <- json_utils/json_utils.py
    safe_load_from_json   <- json_utils/json_utils.py
    safe_read_from_json   <- json_utils/json_utils.py
    write_to_file         <- read_write_utils/read_write_utils.py (local-only)

Notes on the local-only rewrites
---------------------------------
``read_from_file``, ``write_to_file``, ``get_files_and_dirs``, ``is_file`` and
``is_dir`` originally supported remote (SSH) execution through the
``abstract_utilities.ssh_utils`` machinery.  Since that machinery is exactly the
"abstract_utilities module imports" we are removing, those code paths have been
dropped and these functions now operate purely on the local filesystem.  They
still accept ``**kwargs`` so existing call sites that passed remote options will
not raise; the remote options are simply ignored.
"""

import os
import sys
import json
import logging
import importlib
from pathlib import Path
from functools import lru_cache
from logging.handlers import RotatingFileHandler
from typing import Optional


# ---------------------------------------------------------------------------
# type / number helpers
# ---------------------------------------------------------------------------
def is_number(s):
    """Return True if ``s`` can be parsed as a float."""
    try:
        float(s)
        return True
    except Exception:
        return False


def make_list(obj, commaparse=True):
    """
    Convert ``obj`` to a list.

    - A comma-containing string is split on commas (unless ``commaparse`` is False).
    - sets and tuples become lists.
    - lists are returned unchanged.
    - anything else is wrapped in a single-element list.
    """
    if isinstance(obj, str):
        if ',' in obj and commaparse is True:
            obj = obj.split(',')
    if isinstance(obj, (set, tuple)):
        return list(obj)
    if isinstance(obj, list):
        return obj
    return [obj]


# ---------------------------------------------------------------------------
# string helpers  (eatAll + its inner/outer helpers)
# ---------------------------------------------------------------------------
def eatInner(string, list_objects):
    """Strip leading characters that appear in ``list_objects``."""
    if not isinstance(list_objects, list):
        list_objects = [list_objects]
    if not isinstance(string, str):
        string = str(string)
    if string and list_objects:
        for char in string:
            if string:
                if char not in list_objects:
                    return string
                string = string[1:]
    return string


def eatOuter(string, list_objects):
    """Strip trailing characters that appear in ``list_objects``."""
    if not isinstance(list_objects, list):
        list_objects = [list_objects]
    if not isinstance(string, str):
        string = str(string)
    if string and list_objects:
        for _ in range(len(string)):
            if string:
                if string[-1] not in list_objects:
                    return string
                string = string[:-1]
    return string


def eatAll(string, list_objects):
    """Strip leading and trailing characters that appear in ``list_objects``."""
    if not isinstance(list_objects, list):
        list_objects = [list_objects]
    if not isinstance(string, str):
        string = str(string)
    if string and list_objects:
        string = eatInner(string, list_objects)
    if string and list_objects:
        string = eatOuter(string, list_objects)
    return string


# ---------------------------------------------------------------------------
# basic path predicates / joins  (local filesystem only)
# ---------------------------------------------------------------------------
def is_file(path, *args, **kwargs):
    """Local-only check: True if ``path`` is an existing file."""
    if path:
        return os.path.isfile(path)
    return False


def is_dir(path, *args, **kwargs):
    """Local-only check: True if ``path`` is an existing directory."""
    if path:
        return os.path.isdir(path)
    return False


def join_path(directory, basename):
    """Join two path components with ``os.path.join``."""
    return os.path.join(directory, basename)


def get_files(directory):
    """Recursively collect every file path under ``directory``."""
    file_list = []
    for root, _dirs, files in os.walk(directory):
        for file in files:
            file_list.append(os.path.join(root, file))
    return file_list


def get_directory(directory):
    """Return ``directory``, creating it (and parents) if it does not exist."""
    if not os.path.isdir(directory):
        os.makedirs(directory, exist_ok=True)
    return directory


def get_dirlist(directory):
    """
    Return the contents of ``directory``.

    The directory is created if missing.  If ``directory`` happens to be a file,
    a single-element list with its basename is returned.
    """
    path = get_directory(directory)
    if not path:
        return path
    dir_list = []
    if is_dir(path):
        dir_list = os.listdir(path)
    elif is_file(path):
        dir_list = [os.path.basename(path)]
    return dir_list


# ---------------------------------------------------------------------------
# pathlib-aware helpers  (get_home_dir / get_file_parts)
# ---------------------------------------------------------------------------
def is_pathlike(obj):
    return bool(obj) and isinstance(obj, Path)


def get_pathlib_path(obj):
    if obj and not is_pathlike(obj):
        obj = Path(obj)
    return obj


def get_str_path(obj):
    if is_pathlike(obj):
        obj = str(obj)
    return obj


def return_path(path, isPath=False):
    if isPath:
        path = get_pathlib_path(path)
    else:
        path = get_str_path(path)
    return path


def get_home_dir(path=None):
    """Return the home directory, preserving str/Path type of ``path``."""
    isPath = is_pathlike(path)
    path = path or os.getcwd()
    nupath = get_pathlib_path(path)
    home_path = nupath.home()
    return return_path(home_path, isPath=isPath)


def get_safe_basename(path=None):
    if path:
        return os.path.basename(str(path))


def get_safe_dirname(path=None):
    if path:
        isPath = is_pathlike(path)
        dirname = os.path.dirname(str(path))
        return return_path(dirname, isPath=isPath)


def get_safe_splitext(path=None, basename=None):
    basename = basename or get_safe_basename(path=path)
    if basename:
        filename, ext = os.path.splitext(str(basename))
        return filename, ext
    return None, None


def get_file_parts(path):
    """
    Decompose ``path`` into a dict of useful components: basename, filename,
    extension, and the names of the parent / grandparent / great-grandparent
    directories.
    """
    if path:
        path = str(path)
        basename = get_safe_basename(path)
        filename, ext = get_safe_splitext(basename=basename)

        dirname = get_safe_dirname(path)
        dirbase = get_safe_basename(dirname)

        parent_dirname = get_safe_dirname(dirname)
        parent_dirbase = get_safe_basename(parent_dirname)

        super_dirname = get_safe_dirname(parent_dirname)
        super_dirbase = get_safe_basename(super_dirname)

        return {
            "file_path": path,
            "dirname": dirname,
            "basename": basename,
            "filename": filename,
            "ext": ext,
            "dirbase": dirbase,
            "parent_dirname": parent_dirname,
            "parent_dirbase": parent_dirbase,
            "super_dirname": super_dirname,
            "super_dirbase": super_dirbase,
        }


# ---------------------------------------------------------------------------
# directory creation  (makedirs)
# ---------------------------------------------------------------------------
def safe_join(*paths):
    """``os.path.join`` that silently drops falsy components."""
    paths = [path for path in paths if path]
    return os.path.join(*paths)


def raw_create_dirs(*paths):
    """Recursively create every directory along the given path and return it."""
    full_path = os.path.abspath(safe_join(*paths))
    sub_parts = [p for p in full_path.split(os.sep) if p]

    current_path = "/" if full_path.startswith(os.sep) else ""
    for part in sub_parts:
        current_path = safe_join(current_path, part)
        os.makedirs(current_path, exist_ok=True)
    return full_path


# ``makedirs`` in the package is just an alias of ``raw_create_dirs``.
makedirs = raw_create_dirs


# ---------------------------------------------------------------------------
# file collection  (get_files_and_dirs) -- local-only rewrite
# ---------------------------------------------------------------------------
def get_files_and_dirs(*args, recursive=True, include_files=True, **kwargs):
    """
    Return ``(dirs, files)`` collected from one or more directories.

    Args:
        *args: directories to scan (defaults to the current working directory).
        recursive: walk subdirectories when True, otherwise only the top level.
        include_files: include files in the result when True.

    Remote/SSH options accepted by the original implementation are ignored.
    """
    directories = [a for a in args if a] or [os.getcwd()]
    dirs, files = [], []
    for directory in directories:
        if not os.path.isdir(directory):
            continue
        if recursive:
            for root, dnames, fnames in os.walk(directory):
                for d in dnames:
                    dirs.append(os.path.join(root, d))
                if include_files:
                    for fn in fnames:
                        files.append(os.path.join(root, fn))
        else:
            for name in os.listdir(directory):
                full = os.path.join(directory, name)
                if os.path.isdir(full):
                    dirs.append(full)
                elif include_files and os.path.isfile(full):
                    files.append(full)
    return dirs, files


# ---------------------------------------------------------------------------
# read / write  (read_from_file / write_to_file) -- local-only rewrite
# ---------------------------------------------------------------------------
def _write_to_file(contents, file_path, **kwargs):
    os.makedirs(os.path.dirname(file_path) or ".", exist_ok=True)
    with open(file_path, "w", encoding="utf-8") as f:
        f.write(str(contents))
    return file_path


def write_to_file(contents, file_path, **kwargs):
    """Overwrite ``file_path`` with ``contents`` (creating parent dirs)."""
    try:
        return _write_to_file(contents, file_path, **kwargs)
    except Exception as e:
        print("WRITE ERROR:", e)
        raise RuntimeError(f"Failed writing: {file_path}")


def read_from_file(file_path=None, **kwargs):
    """Read and return the text contents of ``file_path``."""
    with open(file_path, "r", encoding="utf-8") as f:
        return f.read()


# ---------------------------------------------------------------------------
# JSON utilities
# ---------------------------------------------------------------------------
import re  # noqa: E402  (kept next to the JSON helpers that use it)

_json_logger = logging.getLogger(__name__)


def try_json_loads(data):
    try:
        return json.loads(data)
    except Exception:
        return None


def safe_json_loads(data):
    if not isinstance(data, dict):
        data = try_json_loads(data) or data
    return data


def clean_invalid_newlines(json_string, line_replacement_value=''):
    """Remove newlines that are not inside double-quoted strings."""
    pattern = r'(?<!\\)\n(?!([^"]*"[^"]*")*[^"]*$)'
    return re.sub(pattern, line_replacement_value, json_string)


def read_malformed_json(json_string, line_replacement_value="*n"):
    """Attempt to parse a (possibly malformed) JSON string after cleaning it."""
    if isinstance(json_string, str):
        json_string = clean_invalid_newlines(json_string, line_replacement_value=line_replacement_value)
    return safe_json_loads(json_string)


def get_value_from_path(json_data, path, line_replacement_value='*n*'):
    """Traverse a nested JSON object along ``path`` and return the value found."""
    current_data = safe_json_loads(json_data)
    for step in path:
        current_data = safe_json_loads(current_data[step])
        if isinstance(current_data, str):
            current_data = read_malformed_json(current_data, line_replacement_value=line_replacement_value)
    return current_data


def find_paths_to_key(json_data, key_to_find, line_replacement_value='*n*'):
    """Return every path (list of keys/indices) that leads to ``key_to_find``."""
    def _search_path(data, current_path):
        if isinstance(data, dict):
            for key, value in data.items():
                new_path = current_path + [key]
                if key == key_to_find:
                    paths.append(new_path)
                if isinstance(value, str):
                    try:
                        nested = read_malformed_json(value, line_replacement_value=line_replacement_value)
                        _search_path(nested, new_path)
                    except json.JSONDecodeError:
                        pass
                _search_path(value, new_path)
        elif isinstance(data, list):
            for index, item in enumerate(data):
                _search_path(item, current_path + [index])

    paths = []
    _search_path(json_data, [])
    return paths


def get_any_value(json_obj, key, line_replacement_value="*n*"):
    """
    Fetch the value(s) associated with ``key`` from a JSON object, JSON string,
    or a path to a JSON file.
    """
    if isinstance(json_obj, str):
        if os.path.isfile(json_obj):
            with open(json_obj, 'r', encoding='UTF-8') as f:
                json_obj = f.read()
    json_data = read_malformed_json(json_obj)
    paths_to_value = find_paths_to_key(json_data, key)
    if not isinstance(paths_to_value, list):
        paths_to_value = [paths_to_value]
    for i, path_to_value in enumerate(paths_to_value):
        paths_to_value[i] = get_value_from_path(json_data, path_to_value)
        if isinstance(paths_to_value[i], str):
            paths_to_value[i] = paths_to_value[i].replace(line_replacement_value, '\n')
    if isinstance(paths_to_value, list):
        if len(paths_to_value) == 0:
            paths_to_value = None
        elif len(paths_to_value) == 1:
            paths_to_value = paths_to_value[0]
    return paths_to_value


def validate_file_path(file_path, is_read=False):
    if file_path and isinstance(file_path, str):
        if os.path.isfile(file_path) or os.path.isdir(file_path):
            return file_path
        if not is_read:
            dirname = os.path.dirname(file_path)
            if os.path.isdir(dirname):
                return file_path


def get_file_path(*args, is_read=False, **kwargs):
    args = list(args)
    for file_path in args:
        if validate_file_path(file_path, is_read=is_read):
            return file_path
    for file_path in list(kwargs.values()):
        if validate_file_path(file_path, is_read=is_read):
            return file_path


def _write_file(data, file_path):
    with open(file_path, 'w', encoding='utf-8') as file:
        file.write(str(data))


def _write_json(data, file_path, ensure_ascii=False, indent=4):
    with open(file_path, 'w', encoding='utf-8') as file:
        json.dump(data, file, ensure_ascii=ensure_ascii, indent=indent)


def _safe_write_json(data, file_path, ensure_ascii=False, indent=4):
    if isinstance(data, (dict, list, tuple)):
        _write_json(data, file_path, ensure_ascii=ensure_ascii, indent=indent)
    else:
        _write_file(data, file_path)


def _read_json(file_path):
    with open(file_path, 'r', encoding='utf-8') as file:
        return json.load(file)


def _output_read_write_error(e, function_name, file_path, valid_file_path=None, data=None, is_read=False):
    error_text = f"Error in {function_name};{e}\nFile path: {file_path} "
    if valid_file_path is None:
        error_text += f"\nValid File path: {valid_file_path} "
    if not is_read:
        error_text += f"\nData: {data} "
    _json_logger.error(error_text)


def safe_dump_to_file(data, file_path=None, ensure_ascii=False, indent=4, *args, **kwargs):
    """Serialize ``data`` to ``file_path`` (JSON for dict/list/tuple, else text)."""
    is_read = False
    file_args = [file_path, data]
    valid_file_path = get_file_path(*file_args, *args, is_read=is_read, **kwargs)

    if valid_file_path:
        file_path = valid_file_path
        if file_path == file_args[-1]:
            data = file_args[0]
    if file_path is not None and data is not None:
        try:
            _safe_write_json(data, file_path, ensure_ascii=ensure_ascii, indent=indent)
        except Exception as e:
            _output_read_write_error(e, 'safe_dump_to_file', file_path, valid_file_path, is_read=is_read)
    else:
        _json_logger.error("file_path and data must be provided to safe_dump_to_file")


def safe_read_from_json(file_path, *args, **kwargs):
    """Read and return JSON content from ``file_path`` (None on failure)."""
    is_read = True
    valid_file_path = get_file_path(file_path, *args, is_read=is_read, **kwargs)
    if valid_file_path:
        file_path = valid_file_path
    try:
        return _read_json(file_path)
    except Exception as e:
        _output_read_write_error(e, 'safe_read_from_json', file_path, valid_file_path, is_read=is_read)
        return None


def safe_load_from_json(*args, **kwargs):
    """Alias for :func:`safe_read_from_json`."""
    return safe_read_from_json(*args, **kwargs)


def safe_dump_to_json(*args, **kwargs):
    """Alias for :func:`safe_dump_to_file`."""
    return safe_dump_to_file(*args, **kwargs)

def flatten_json(data, parent_key='', sep='_'):
    """
    Flatten a JSON object into a single dictionary with keys indicating the nested structure.

    Args:
        data (dict): The JSON object to flatten.
        parent_key (str): The base key to use for nested keys (used in recursive calls).
        sep (str): The separator to use between keys.

    Returns:
        dict: The flattened JSON object.
    """
    items = []
    if isinstance(data, dict):
        for key, value in data.items():
            new_key = f"{parent_key}{sep}{key}" if parent_key else key
            if isinstance(value, dict):
                items.extend(flatten_json(value, new_key, sep=sep).items())
            elif isinstance(value, list):
                for i, item in enumerate(value):
                    items.extend(flatten_json(item, f"{new_key}{sep}{i}", sep=sep).items())
            else:
                items.append((new_key, value))
    elif isinstance(data, list):
        for i, item in enumerate(data):
            items.extend(flatten_json(item, f"{parent_key}{sep}{i}", sep=sep).items())
    else:
        items.append((parent_key, data))

    return dict(items)
# ---------------------------------------------------------------------------
# media type detection  (derive_media_type)
# ---------------------------------------------------------------------------
MIME_TYPES = {
    'image': {
        '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png',
        '.gif': 'image/gif', '.bmp': 'image/bmp', '.tiff': 'image/tiff',
        '.webp': 'image/webp', '.svg': 'image/svg+xml', '.ico': 'image/x-icon',
        '.heic': 'image/heic', '.psd': 'image/vnd.adobe.photoshop',
        '.raw': 'image/x-raw', '.apng': 'image/apng', '.heif': 'image/heif',
        '.jp2': 'image/jp2', '.jxl': 'image/jxl', '.eps': 'application/postscript',
        '.ai': 'application/postscript',
    },
    'video': {
        '.mp4': 'video/mp4', '.webm': 'video/webm', '.ogg': 'video/ogg',
        '.mov': 'video/quicktime', '.avi': 'video/x-msvideo', '.mkv': 'video/x-matroska',
        '.flv': 'video/x-flv', '.wmv': 'video/x-ms-wmv', '.3gp': 'video/3gpp',
        '.mpeg': 'video/mpeg', '.mpg': 'video/mpg', '.m4v': 'video/x-m4v',
        '.f4v': 'video/x-f4v', '.asf': 'video/x-ms-asf', '.vob': 'video/dvd',
        '.m2ts': 'video/mp2t', '.mts': 'video/mp2t',
    },
    'audio': {
        '.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.flac': 'audio/flac',
        '.aac': 'audio/aac', '.ogg': 'audio/ogg', '.m4a': 'audio/mp4',
        '.opus': 'audio/opus', '.aif': 'audio/x-aiff', '.aiff': 'audio/x-aiff',
        '.amr': 'audio/amr', '.mid': 'audio/midi', '.midi': 'audio/midi',
        '.wma': 'audio/x-ms-wma', '.mka': 'audio/x-matroska',
    },
    'document': {
        '.pdf': 'application/pdf', '.doc': 'application/msword',
        '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
        '.odt': 'application/vnd.oasis.opendocument.text', '.txt': 'text/plain',
        '.rtf': 'application/rtf', '.md': 'text/markdown', '.markdown': 'text/markdown',
        '.tex': 'application/x-tex', '.log': 'text/plain', '.json': 'application/json',
        '.xml': 'application/xml', '.yaml': 'application/x-yaml', '.yml': 'application/x-yaml',
        '.ini': 'text/plain', '.cfg': 'text/plain', '.toml': 'application/toml',
        '.csv': 'text/csv', '.tsv': 'text/tab-separated-values', '.epub': 'application/epub+zip',
        '.mobi': 'application/x-mobipocket-ebook', '.azw': 'application/vnd.amazon.ebook',
        '.pages': 'application/x-iwork-pages-sffpages',
        '.numbers': 'application/x-iwork-numbers-sffnumbers',
        '.key': 'application/x-iwork-keynote-sffkey',
    },
    'presentation': {
        '.ppt': 'application/vnd.ms-powerpoint',
        '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
        '.odp': 'application/vnd.oasis.opendocument.presentation',
    },
    'spreadsheet': {
        '.xls': 'application/vnd.ms-excel',
        '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
        '.ods': 'application/vnd.oasis.opendocument.spreadsheet',
        '.csv': 'text/csv', '.tsv': 'text/tab-separated-values',
    },
    'data_science': {
        '.parquet': 'application/x-parquet', '.avro': 'application/avro',
        '.hdf5': 'application/x-hdf5', '.h5': 'application/x-hdf5',
        '.pickle': 'application/octet-stream', '.npy': 'application/x-npy',
        '.npz': 'application/x-npz', '.ipynb': 'application/x-ipynb+json',
        '.sqlite': 'application/x-sqlite3', '.db': 'application/x-sqlite3',
    },
    'code': {
        '.py': 'text/x-python', '.java': 'text/x-java-source', '.c': 'text/x-c',
        '.cpp': 'text/x-c++', '.h': 'text/x-c', '.hpp': 'text/x-c++',
        '.js': 'application/javascript', '.cjs': 'application/javascript',
        '.mjs': 'application/javascript', '.jsx': 'application/javascript',
        '.ts': 'application/javascript', '.tsx': 'application/typescript',
        '.rb': 'text/x-ruby', '.php': 'application/x-php', '.go': 'text/x-go',
        '.rs': 'text/rust', '.swift': 'text/x-swift', '.kt': 'text/x-kotlin',
        '.sh': 'application/x-shellscript', '.bash': 'application/x-shellscript',
        '.ps1': 'application/x-powershell', '.sql': 'application/sql',
        '.yml': 'application/x-yaml', '.coffee': 'text/coffeescript', '.lua': 'text/x-lua',
        '.css': 'text/css', '.scss': 'text/x-scss', '.sass': 'text/x-sass',
        '.less': 'text/x-less', '.html': 'text/html', '.htm': 'text/html',
        '.vue': 'text/x-vue', '.svelte': 'text/x-svelte', '.graphql': 'application/graphql',
        '.gql': 'application/graphql', '.dockerfile': 'text/x-dockerfile',
        '.makefile': 'text/x-makefile', '.sol': 'text/x-solidity',
    },
    'three_d': {
        '.obj': 'model/obj', '.stl': 'model/stl', '.glb': 'model/gltf-binary',
        '.gltf': 'model/gltf+json', '.fbx': 'application/octet-stream',
        '.usd': 'model/usd', '.usdz': 'model/vnd.usdz+zip', '.blend': 'application/x-blender',
    },
    'archive': {
        '.zip': 'application/zip', '.tar': 'application/x-tar', '.gz': 'application/gzip',
        '.tgz': 'application/gzip', '.bz2': 'application/x-bzip2', '.xz': 'application/x-xz',
        '.rar': 'application/vnd.rar', '.7z': 'application/x-7z-compressed',
        '.iso': 'application/x-iso9660-image', '.dmg': 'application/x-apple-diskimage',
        '.jar': 'application/java-archive', '.war': 'application/java-archive',
        '.whl': 'application/python-wheel', '.egg': 'application/python-egg',
        '.zpaq': 'application/x-zpaq',
    },
    'font': {
        '.ttf': 'font/ttf', '.otf': 'font/otf', '.woff': 'font/woff',
        '.woff2': 'font/woff2', '.eot': 'application/vnd.ms-fontobject',
    },
    'executable': {
        '.exe': 'application/vnd.microsoft.portable-executable',
        '.dll': 'application/vnd.microsoft.portable-executable',
        '.bin': 'application/octet-stream', '.deb': 'application/vnd.debian.binary-package',
        '.rpm': 'application/x-rpm', '.app': 'application/x-executable',
        '.ipa': 'application/x-itunes-ipa', '.apk': 'application/vnd.android.package-archive',
    },
    'geospatial': {
        '.geojson': 'application/geo+json', '.kml': 'application/vnd.google-earth.kml+xml',
        '.kmz': 'application/vnd.google-earth.kmz', '.shp': 'application/x-qgis-main-file',
        '.shx': 'application/x-qgis-shape-index', '.dbf': 'application/x-dbf',
        '.gpkg': 'application/geopackage+sqlite3', '.gpx': 'application/gpx+xml',
        '.tif': 'image/tiff', '.tiff': 'image/tiff', '.osm': 'application/xml',
        '.wkt': 'text/plain',
    },
    'pandas_data': {
        '.parquet': 'application/x-parquet', '.feather': 'application/x-feather',
        '.orc': 'application/x-orc', '.hdf': 'application/x-hdf', '.h5': 'application/x-hdf5',
        '.pickle': 'application/octet-stream', '.pkl': 'application/octet-stream',
        '.msgpack': 'application/x-msgpack', '.stata': 'application/x-stata',
        '.dta': 'application/x-stata', '.sas7bdat': 'application/x-sas-data',
        '.sav': 'application/x-spss-sav',
    },
}

# Category -> set of extensions (equivalent to the package's MEDIA_TYPES).
MEDIA_TYPES = {category: set(mapping.keys()) for category, mapping in MIME_TYPES.items()}


def derive_media_type(obj):
    """Return the media category (e.g. 'image', 'video') for a path or extension."""
    ext = os.path.splitext(str(obj))[-1] or obj
    if ext:
        for typ, exts in MEDIA_TYPES.items():
            if ext in exts:
                return typ


# ---------------------------------------------------------------------------
# caller introspection  (get_caller_dir)
# ---------------------------------------------------------------------------
import inspect  # noqa: E402  (kept next to the caller helpers that use it)


def get_caller(i=None):
    """Return the filename of the calling frame ``i`` levels up (default 1)."""
    depth = 1 if i is None else int(i)
    stack = inspect.stack()
    if depth >= len(stack):
        depth = len(stack) - 1
    return stack[depth].filename


def get_caller_path(i=None):
    """Return the absolute, real path of the caller's file."""
    depth = 1 if i is None else int(i)
    file_path = get_caller(depth + 1)
    return os.path.realpath(file_path)


def get_caller_dir(i=None):
    """Return the absolute directory of the caller's file."""
    depth = 1 if i is None else int(i)
    abspath = get_caller_path(depth + 1)
    return os.path.dirname(abspath)


# ---------------------------------------------------------------------------
# environment values  (get_env_value) -- standalone, no abstractEnv class
# ---------------------------------------------------------------------------
def _split_eq(line):
    """Split a ``KEY=VALUE`` line into a cleaned ``[key, value]`` pair."""
    if '=' in line:
        key_side = line.split('=')[0]
        value_side = line[len(key_side + '='):]
        return [eatOuter(key_side, [' ', '', '\t']),
                eatAll(value_side, [' ', '', '\t', '\n'])]
    return [line, None]


def _search_for_env_key(key, path, deep_scan=False):
    """Return the value for ``key`` within the env file at ``path`` (or None)."""
    highest = [None, 0]
    if path and os.path.isfile(path):
        with open(path, "r") as f:
            for line in f:
                line_key, line_value = _split_eq(line)
                if line_key == key:
                    return line_value
                if deep_scan and key:
                    key_parts = 0
                    for key_part in key.split('_'):
                        if key_part and key_part in line_key:
                            key_parts += len(key_part)
                    if float(key_parts / len(key)) >= 0.5 and key_parts > highest[1]:
                        highest = [line_value, key_parts]
                        return line_value
    return None


def get_env_value(key=None, path=None, file_name=None, deep_scan=False):
    """
    Retrieve the value of an environment variable from a ``.env`` style file.

    Searches, in order: the supplied ``path``, the current working directory,
    the user's home directory, and ``~/.envy_all``.

    Args:
        key: variable name to look up (defaults to ``MY_PASSWORD``).
        path: a directory to search, or a direct path to an env file.
        file_name: env file name (defaults to ``.env``).
        deep_scan: when True, fall back to fuzzy key matching.
    """
    key = key or 'MY_PASSWORD'
    file_name = file_name or '.env'
    current_folder = os.getcwd()

    if path and os.path.isfile(path):
        file_name = os.path.basename(path)
        path = os.path.dirname(path)
    else:
        path = path or current_folder

    home_folder = os.path.expanduser("~")
    envy_all = os.path.join(home_folder, '.envy_all')

    directories = []
    for directory in [path, current_folder, home_folder, envy_all]:
        if directory and os.path.isdir(directory) and directory not in directories:
            directories.append(directory)

    for directory in directories:
        env_path = os.path.join(directory, file_name)
        if os.path.isfile(env_path):
            value = _search_for_env_key(key=key, path=env_path, deep_scan=deep_scan)
            if value:
                return value
    return None


# ---------------------------------------------------------------------------
# dataclass construction helper  (get_inputs)
# ---------------------------------------------------------------------------
def get_inputs(cls, *args, **kwargs):
    """
    Dynamically construct a (data)class instance from ``args``/``kwargs``,
    filling any missing values from the class's own defaults.
    """
    fields = list(cls.__annotations__.keys())
    values = {}

    args = list(args)
    for field in fields:
        if field in kwargs:
            values[field] = kwargs[field]
        elif args:
            values[field] = args.pop(0)
        else:
            values[field] = getattr(cls(), field)  # default from the class

    return cls(**values)


# ---------------------------------------------------------------------------
# lazy import  (lazy_import)
# ---------------------------------------------------------------------------
nullProxy_logger = logging.getLogger("abstract.lazy_import")


class nullProxy:
    """Safe, chainable, callable placeholder returned for missing modules/attrs."""

    def __init__(self, name, path=(), fallback=None):
        self._name = name
        self._path = path
        self.fallback = fallback

    def __getattr__(self, attr):
        return nullProxy(self._name, self._path + (attr,))

    def __call__(self, *args, **kwargs):
        if self.fallback is not None:
            try:
                return self.fallback(*args, **kwargs)
            except Exception as e:
                nullProxy_logger.info("%s", e)
        nullProxy_logger.warning(
            "[lazy_import] Call to missing module/attr: %s.%s args=%s kwargs=%s",
            self._name, ".".join(self._path), args, kwargs,
        )
        return None

    def __repr__(self):
        full = ".".join((self._name, *self._path))
        return f"<nullProxy {full}>"

    def __bool__(self):
        return False


@lru_cache(maxsize=None)
def lazy_import_single(name, fallback=None):
    """Import ``name`` safely, returning a :class:`nullProxy` if unavailable."""
    if name in sys.modules:
        return sys.modules[name]
    try:
        return importlib.import_module(name)
    except Exception as e:
        nullProxy_logger.warning("[lazy_import] Failed to import '%s': %s", name, e)
        return nullProxy(name, fallback=fallback)


def get_lazy_attr(module_name, *attrs, fallback=None):
    obj = lazy_import(module_name, fallback=fallback)
    for attr in attrs:
        try:
            obj = getattr(obj, attr)
        except Exception:
            return nullProxy(module_name, attrs, fallback=fallback)
    return obj


def lazy_import(name, *attrs, fallback=None):
    """
    Import a module (and optionally walk into ``attrs``) safely.

    Returns the module/attribute, or a :class:`nullProxy` placeholder if the
    import fails — so the result is always safe to reference.
    """
    if attrs:
        return get_lazy_attr(name, *attrs, fallback=fallback)
    return lazy_import_single(name, fallback=fallback)


# ---------------------------------------------------------------------------
# logging  (get_logFile)
# ---------------------------------------------------------------------------
_PACKAGE_NAME = "abstract_utilities"

LOG_FORMAT = (
    "[%(asctime)s] "
    "%(levelname)-8s "
    "%(name)s:%(lineno)d | "
    "%(message)s "
    "[target=%(target_file)s:%(target_line)s]"
)
DATE_FORMAT = "%Y-%m-%d %H:%M:%S"


class LevelFilter(logging.Filter):
    """Filter that allows selective level enablement/disablement."""

    def __init__(self):
        super().__init__()
        self.enabled_levels = {
            logging.DEBUG, logging.INFO, logging.WARNING,
            logging.ERROR, logging.CRITICAL,
        }

    def filter(self, record):
        return record.levelno in self.enabled_levels

    def enable_level(self, level):
        self.enabled_levels.add(level)

    def disable_level(self, level):
        self.enabled_levels.discard(level)

    def is_enabled(self, level):
        return level in self.enabled_levels


class SafeFormatter(logging.Formatter):
    """Formatter that tolerates missing ``target_*`` extra fields."""

    def format(self, record):
        record.target_file = getattr(record, "target_file", "-")
        record.target_line = getattr(record, "target_line", "-")
        return super().format(record)


def _resolve_log_root():
    venv = os.getenv("VIRTUAL_ENV") or os.getenv("CONDA_PREFIX")
    if venv:
        p = Path(venv) / ".logs" / _PACKAGE_NAME
        p.mkdir(parents=True, exist_ok=True)
        return p

    home = Path.home() / ".cache" / _PACKAGE_NAME / "logs"
    try:
        home.mkdir(parents=True, exist_ok=True)
        return home
    except PermissionError:
        pass

    try:
        syslog = Path("/var/log") / _PACKAGE_NAME
        syslog.mkdir(parents=True, exist_ok=True)
        return syslog
    except PermissionError:
        fallback = Path("/tmp") / _PACKAGE_NAME / "logs"
        fallback.mkdir(parents=True, exist_ok=True)
        return fallback


LOG_ROOT = _resolve_log_root()

# Filters keyed by logger name for runtime control.
_level_filters = {}


def get_logFile(name, *, level=logging.INFO, console=True,
                max_bytes=5 * 1024 * 1024, backup_count=5):
    """
    Return a configured rotating-file logger named ``name``.

    The logger writes to ``LOG_ROOT/<name>.log`` and (optionally) the console,
    using :class:`SafeFormatter` and a per-logger :class:`LevelFilter`.
    """
    logger = logging.getLogger(name)

    # Skip re-initialization if already configured.
    if logger.handlers:
        return logger

    logger.setLevel(logging.DEBUG)  # Logger accepts everything.

    formatter = SafeFormatter(LOG_FORMAT, DATE_FORMAT)

    level_filter = LevelFilter()
    if level > logging.DEBUG:
        level_filter.disable_level(logging.DEBUG)
    _level_filters[name] = level_filter

    try:
        file_handler = RotatingFileHandler(
            LOG_ROOT / f"{name}.log",
            maxBytes=max_bytes,
            backupCount=backup_count,
            encoding="utf-8",
        )
        file_handler.setFormatter(formatter)
        file_handler.setLevel(logging.DEBUG)
        file_handler.addFilter(level_filter)
        logger.addHandler(file_handler)
    except PermissionError:
        logger.addHandler(logging.NullHandler())

    if console:
        console_handler = logging.StreamHandler(sys.stdout)
        console_handler.setFormatter(formatter)
        console_handler.setLevel(logging.DEBUG)
        console_handler.addFilter(level_filter)
        logger.addHandler(console_handler)

    logger.propagate = False
    return logger


# ---------------------------------------------------------------------------
# SingletonMeta  (abstract_utilities.SingletonMeta)
# ---------------------------------------------------------------------------
import threading as _threading  # noqa: E402


class SingletonMeta(type):
    """Thread-safe singleton metaclass: one instance per class."""

    _instances: dict = {}
    _lock = _threading.Lock()

    def __call__(cls, *args, **kwargs):
        with cls._lock:
            if cls not in cls._instances:
                cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]


# ---------------------------------------------------------------------------
# parse_url  (abstract_utilities url helper)
# ---------------------------------------------------------------------------
from urllib.parse import urlparse as _urlparse  # noqa: E402


def parse_url(url):
    """Decompose a URL into a dict of components.

    ``path`` is returned without its leading slash so ``path.split('/')[0]``
    yields the first path segment (e.g. the owner in an ``owner/repo`` URL),
    matching how the original abstract_utilities helper was consumed.
    """
    p = _urlparse(str(url or ""))
    return {
        "scheme": p.scheme,
        "netloc": p.netloc,
        "host": p.hostname,
        "port": p.port,
        "path": (p.path or "").strip("/"),
        "params": p.params,
        "query": p.query,
        "fragment": p.fragment,
    }


##__all__ = [
##    "derive_media_type",
##    "eatAll",
##    "get_any_value",
##    "get_caller_dir",
##    "get_dirlist",
##    "get_env_value",
##    "get_file_parts",
##    "get_files",
##    "get_files_and_dirs",
##    "get_home_dir",
##    "get_inputs",
##    "get_logFile",
##    "is_dir",
##    "is_file",
##    "is_number",
##    "join_path",
##    "lazy_import",
##    "make_list",
##    "makedirs",
##    "nullProxy",
##    "parse_url",
##    "read_from_file",
##    "safe_dump_to_file",
##    "safe_dump_to_json",
##    "safe_load_from_json",
##    "safe_read_from_json",
##    "SingletonMeta",
##    "write_to_file",
##    "flatten_json"
##]

# ===========================================================================
# Appended surface (list helpers + kwarg/param utilities + socket re-export)
# so abstract_essentials covers the full abstract_utilities surface that
# downstream packages (abstract_flask, abstract_apis) consume.
# ===========================================================================
import socket  # re-exported for back-compat: consumers do `from ..imports import socket`


def get_sort(ls: list, k: int = 0):
    ls.sort()
    return ls[k]


def combineList(ls: list, lsN: list) -> list:
    for k in range(len(lsN)):
        ls.append(lsN[k])
    return ls


def find_original_case(input_list, search_string):
    for obj in input_list:
        if obj.lower() == search_string.lower():
            return obj
    return None


def ensure_nested_list(obj):
    if not isinstance(obj, list):
        return [obj]
    for element in obj:
        if not isinstance(element, list):
            return [obj]
    return obj


def make_list_add(obj, values):
    obj = list(obj)
    for each in list(values):
        obj.append(each)
    return obj


def recursive_json_list(json_list: dict, desired_values: dict) -> list:
    recursed_list = []
    for json_obj in json_list:
        bool_count = 0
        for desired_key, desired_value in desired_values.items():
            if desired_key in json_obj:
                if json_obj[desired_key] == desired_value:
                    bool_count += 1
        if bool_count == len(list(desired_values.keys())):
            recursed_list.append(json_obj)
    return recursed_list


def filter_json_list_values(json_list: list, keys: list):
    all_keys = {}
    for key in keys:
        unique_values = []
        for json_obj in json_list:
            if key in json_obj:
                if json_obj[key] not in unique_values:
                    unique_values.append(json_obj[key])
        all_keys[key] = unique_values
    return all_keys


def get_highest_value_obj(obj_list, function):
    return max(obj_list, key=function)


def safe_list_return(current_list, list_num=0):
    if len(current_list) >= list_num + 1:
        return current_list[int(list_num)]


def get_actual_number(reference_object, number_value):
    try:
        reference_length = len(reference_object)
    except Exception:
        reference_length = 0
    if reference_length == 0:
        return None
    return max(0, min(number_value, reference_length - 1))


def compare_lists(list_1, list_2):
    if len(list_1) > len(list_2):
        return False
    for each in list_1:
        if each not in list_2:
            return False
    return True


def remove_from_list(list_obj, key, value):
    return [obj for obj in list_obj if not (isinstance(obj, dict) and obj.get(key) == value)]


def list_set(obj):
    try:
        obj = list(set(obj))
    except Exception as e:
        print(f"{e}")
    return obj


def get_symetric_difference(obj_1, obj_2):
    set1 = set(obj_1)
    set2 = set(obj_2)
    return list(set1.symmetric_difference(set2))


def make_list_it(obj=None):
    return make_list(obj or [])


def get_single_from_list(list_obj, default=None):
    return make_list(list_obj or default)[0]


def get_keys(mapping, typ=None):
    typ = typ or set
    if isinstance(mapping, dict):
        mapping = mapping.keys()
    return typ(mapping)


def make_key_map(dict_obj):
    return {k: get_keys(v) for k, v in dict_obj.items()}


def get_only_kwargs(varList, *args, **kwargs):
    new_kwargs = {}
    for i, arg in enumerate(args):
        key_variable = varList[i]
        kwargs[key_variable] = arg
    for key, value in kwargs.items():
        if key in varList:
            new_kwargs[key] = value
    return new_kwargs


def get_desired_key_values(obj, keys=None, defaults=None):
    defaults = defaults or {}
    if keys is None:
        return obj
    new_dict = {}
    for key, value in defaults.items():
        new_dict[key] = obj.get(key) or defaults.get(key)
    if obj and isinstance(obj, dict):
        for key in keys:
            new_dict[key] = obj.get(key) or defaults.get(key)
    return new_dict


def dump_if_json(obj):
    if isinstance(obj, dict):
        return json.dumps(obj)
    return obj


def makeParams(*arg, **kwargs):
    arg = make_list(arg)
    arg.append({k: v for k, v in kwargs.items() if v is not None})
    return arg


def prune_inputs(func, *args, **kwargs):
    sig = inspect.signature(func)
    params = sig.parameters
    has_varargs = any(p.kind == inspect.Parameter.VAR_POSITIONAL for p in params.values())
    has_varkw = any(p.kind == inspect.Parameter.VAR_KEYWORD for p in params.values())
    new_args = list(args)
    new_kwargs = dict(kwargs)
    if "args" in new_kwargs:
        explicit_args = new_kwargs.pop("args")
        if isinstance(explicit_args, (list, tuple)):
            new_args.extend(explicit_args)
        else:
            new_args.append(explicit_args)
    if has_varargs:
        preferred_as_args = {"path", "file", "file_path", "filename", "value"}
        positional_candidates = []
        for k in list(new_kwargs.keys()):
            v = new_kwargs[k]
            if k in preferred_as_args:
                positional_candidates.append(v)
                del new_kwargs[k]
            elif isinstance(v, (str, int, float)) and len(positional_candidates) == 0:
                positional_candidates.append(v)
                del new_kwargs[k]
        new_args.extend(positional_candidates)
    if not has_varkw:
        allowed = {
            name for name, p in params.items()
            if p.kind in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY)
        }
        new_kwargs = {k: v for k, v in new_kwargs.items() if k in allowed}
    return tuple(new_args), new_kwargs


def run_pruned_func(func, *args, **kwargs):
    args, kwargs = prune_inputs(func, *args, **kwargs)
    return func(*args, **kwargs)


# Category -> set of extensions, derived from the MIME_TYPES defined above.
MEDIA_TYPES = make_key_map(MIME_TYPES)

__all__ = [
    "is_number", "run_pruned_func", "prune_inputs", "makeParams", "dump_if_json",
    "get_desired_key_values", "get_only_kwargs", "derive_media_type", "eatAll",
    "get_any_value", "get_caller_dir", "get_dirlist", "get_env_value", "get_file_parts",
    "get_files", "get_files_and_dirs", "get_home_dir", "get_inputs", "get_logFile",
    "is_dir", "is_file", "join_path", "lazy_import", "make_list", "makedirs", "nullProxy",
    "parse_url", "read_from_file", "safe_dump_to_file", "safe_dump_to_json",
    "safe_load_from_json", "safe_read_from_json", "SingletonMeta", "write_to_file",
    "MIME_TYPES", "get_sort", "combineList", "find_original_case", "ensure_nested_list",
    "make_list_add", "recursive_json_list", "filter_json_list_values", "get_highest_value_obj",
    "safe_list_return", "get_actual_number", "compare_lists", "remove_from_list", "list_set",
    "get_symetric_difference", "make_list_it", "get_single_from_list", "get_keys", "make_key_map",
    "json", "os", "logging", "sys", "Path", "socket","flatten_json","get_mime_type",
]


def get_mime_type(obj):
    """Return the MIME type string (e.g. 'image/png') for a path/extension, or None."""
    ext = os.path.splitext(str(obj))[-1].lower() or str(obj).lower()
    for mapping in MIME_TYPES.values():
        if isinstance(mapping, dict) and ext in mapping:
            return mapping[ext]
    return None


if "get_mime_type" not in __all__:
    __all__.append("get_mime_type")


# ---------------------------------------------------------------------------
# lazy_module  (TRUE deferred import — imports on first attribute access/call,
# unlike lazy_import which imports immediately-but-safely)
# ---------------------------------------------------------------------------
class _LazyModuleProxy:
    """Defer ``import name`` until the first attribute access or call.

    Degrades to :class:`nullProxy` if the import ultimately fails, so a missing
    optional dependency neither loads at import time nor crashes at import time.
    """
    __slots__ = ("_lm_name", "_lm_fallback", "_lm_mod")

    def __init__(self, name, fallback=None):
        object.__setattr__(self, "_lm_name", name)
        object.__setattr__(self, "_lm_fallback", fallback)
        object.__setattr__(self, "_lm_mod", None)

    def _lm_resolve(self):
        mod = object.__getattribute__(self, "_lm_mod")
        if mod is None:
            name = object.__getattribute__(self, "_lm_name")
            if name in sys.modules:
                mod = sys.modules[name]
            else:
                try:
                    mod = importlib.import_module(name)
                except Exception as e:
                    nullProxy_logger.warning("[lazy_module] failed to import '%s': %s", name, e)
                    mod = nullProxy(name, fallback=object.__getattribute__(self, "_lm_fallback"))
            object.__setattr__(self, "_lm_mod", mod)
        return mod

    def __getattr__(self, attr):
        return getattr(self._lm_resolve(), attr)

    def __call__(self, *args, **kwargs):
        return self._lm_resolve()(*args, **kwargs)

    def __repr__(self):
        return f"<lazy_module {object.__getattribute__(self, '_lm_name')!r}>"


def lazy_module(name, fallback=None):
    """Return a proxy that imports ``name`` on FIRST use (true deferral).

    Unlike :func:`lazy_import` (imports immediately, nullProxy only on failure),
    this stays unloaded until the first attribute access or call::

        pd = lazy_module("pandas")     # pandas NOT imported here
        df = pd.DataFrame(rows)        # imported now, on first use
    """
    return _LazyModuleProxy(name, fallback=fallback)


if "lazy_module" not in __all__:
    __all__.append("lazy_module")
