"""
library for file opertations
"""
import ast
import glob
import json
import logging
import os
import re
import shutil
from pathlib import Path, PurePath, PurePosixPath, PosixPath, PureWindowsPath

import chardet
import errno

import lxml
import requests

GOOGLE_SERVICE_URL="https://8.8.8.8"
GOOGLE_SERVICE_NAME="Internet (Google DNS)"

GOOGLE_HEADERS = {
    "service_url":GOOGLE_SERVICE_URL,
    "service_name":GOOGLE_SERVICE_NAME,
}
HEADERS = {
    "User-Agent": "Mozilla/5.0 (compatible; Pyami/0.1; +https://github.com/petermr/pyami/)"
}
# wildcards
STARS = "**"
STAR = "*"

# suffixes
S_PDF = "pdf"
S_PNG = "png"
S_SVG = "pdf"
S_TXT = "txt"
S_XML = "xml"

# markers for processing
# _NULL = "_NULL"
# _REQD = "_REQD"

# known section names
# SVG = "svg"
# PDFIMAGES = "pdfimages"
# RESULTS = "results"
# SECTIONS = "sections"

# subsects
# IMAGE_STAR = "image*"

# subsects
# OCTREE = "*octree"

# results
# SEARCH = "search"
# WORD = "word"
# EMPTY = "empty"

# files
# FULLTEXT_PAGE = "fulltext-page*"
# CHANNEL_STAR = "channel*"
# RAW = "raw"


# def get_logger_old(cls, filename, file_level=2, suffix=".py", level=logging.INFO):
#     """creates logger for module, uses modulae name
#     removes .py
#     retains level of hierarchy
#     e.g. foo/bar/junk.py with levels = 2 => bar.junk
#     :param filename: to act as logger name
#     :param file_level: to include in hierarchy
#     :param level: logging level (default INFO)
#     :param suffix: suffix to remove, e.g. ".py"
#     """
#     # logger.debug(f"============= file {__file__} name {__name__} =============")
#     if filename:
#         if filename[-len(suffix):] == suffix:
#             filename = filename[:-len(suffix)]
#         module = '.'.join(filename.split(os.path.sep)[-file_level:])
#         logger = logging.getLogger(module)
#
#         # Create handlers for logging to the standard output and a file
#         stdoutHandler = logging.StreamHandler(stream=sys.stdout)
#         errHandler = logging.FileHandler("error.log")
#
#         # Set the log levels on the handlers
#         stdoutHandler.setLevel(logging.DEBUG)
#         errHandler.setLevel(logging.ERROR)
#
#         # Create a log format using Log Record attributes
#         fmt = logging.Formatter(
#             "%(name)s: %(asctime)s | %(levelname)s | %(filename)s:%(lineno)s | %(process)d >>> %(message)s"
#         )
#
#         # Set the log format on each handler
#         stdoutHandler.setFormatter(fmt)
#         errHandler.setFormatter(fmt)
#
#         # Add each handler to the Logger object
#         logger.addHandler(stdoutHandler)
#         logger.addHandler(errHandler)
#
#         logger.info("Server started listening on port 8080")
#         logger.warning(
#             "Disk space on drive '/var/log' is running low. Consider freeing up space"
#         )
#
#         try:
#             raise Exception("Failed to connect to database: 'my_db'")
#         except Exception as e:
#             # exc_info=True ensures that a Traceback is included
#             logger.error(e, exc_info=True)
#
#         logger.info(f"created logger {module} {logger}")
#         return logger
#
logger = logging.getLogger(__name__)


class FileLib:

    @classmethod
    def force_mkdir(cls, dirx):
        """ensure dirx and its parents exist
        :dirx: directory
        """
        path = Path(dirx)
        if not path.exists():
            try:
                path.mkdir(parents=True, exist_ok=True)
                assert (f := path).exists(), f"dir {path} should now exist"
            except Exception as e:
                logger.error(f"cannot make dirx {dirx} , {e}")
                logger.debug(f"cannot make dirx {dirx}, {e}")

    @classmethod
    def force_mkparent(cls, file):
        """ensure parent directory exists

        :path: whose parent directory is to be created if absent
        """
        if file is not None:
            cls.force_mkdir(cls.get_parent_dir(file))

    @classmethod
    def force_write(cls, file, data, overwrite=True):
        """:write path, creating directory if necessary
        :path: path to write to
        :data: str data to write
        :overwrite: force write if path exists

        may throw exception from write
        """
        if file is not None:
            if os.path.exists(file) and not overwrite:
                logging.warning(f"not overwriting existsnt path {file}")
            else:
                cls.force_mkparent(file)
                with open(file, "w", encoding="utf-8") as f:
                    f.write(data)

    @classmethod
    def copy_file_or_directory(cls, dest_path, src_path, overwrite=False):
        """
        copy src_path to dest_path, allowing for overwrite
        :param dest_path: destination file or (complete) directory
        :param src_path: source file or directory tree (must exist)
        :param overwrite: if true foces overwrite of existing dest (default False)
        :except: src files do not exist or overwrite forbidden (or other file error)
        """
        if src_path is None or not src_path.exists():
            raise FileExistsError(f"no src path found {src_path}")
        if dest_path is None:
            raise ValueError(f"dest_path must not be None")
        dest_path = Path(dest_path)
        src_path = Path(src_path)
        if not src_path.exists():
            raise FileNotFoundError(f"src_path must exist {src_path}")
        if dest_path.exists():
            if not overwrite:
                file_type = "dirx" if dest_path.is_dir() else "path"
                raise TypeError(
                    str(dest_path), f"cannot overwrite existing {file_type} (str({dest_path})")

        else:
            dest_parent = dest_path.parent
            if src_path.is_dir():
                logger.warning(f"create directory {dest_path}")
                dest_path.mkdir(parents=True, exist_ok=True)
                logger.info(f"created directory {dest_path}")
            else:
                src_parent = src_path.parent

        if src_path.is_dir():
            if os.path.exists(dest_path):
                shutil.rmtree(dest_path)
            shutil.copytree(src_path, dest_path)
            logger.info(f"copied directory {src_path} to {dest_path}")
        else:
            if dest_path.is_dir() and not overwrite:
                raise FileExistsError(f"cannot overwrite dir {dest_path} with file {src_path}")
            try:
                shutil.copy(src_path, dest_path)  # will overwrite
                logger.info(f"copied path {src_path} to {dest_path}")
            except Exception as e:
                logger.fatal(f"Cannot copy directory {src_path} to {dest_path} because {e}")

    @staticmethod
    def create_absolute_name(file):
        """create absolute/relative name for a path relative to amilib

        TODO this is messy
        """
        absolute_file = None
        if file is not None:
            file_dir = FileLib.get_parent_dir(__file__)
            absolute_file = os.path.join(os.path.join(file_dir, file))
        return absolute_file

    @classmethod
    def get_parent_dir(cls, file):
        return None if file is None else PurePath(file).parent

    @classmethod
    def read_pydictionary_using_ast(cls, file: Path) -> dict:
        """read a dumped dictionary into a python dictionary
        when a dict is serialised it is not JSON and needs this
        to read it.

        :param file: dictionary file to read
        :return: JSON dictionary (created by ast.literal_eval)
        """
        with open(file, "r") as f:
            read = f.read()
            pydict = ast.literal_eval(read)
        return pydict


    @classmethod
    def punct2underscore(cls, text):
        """ replace all ASCII punctuation except '.' , '-', '_' by '_'

        usually used for filenames
        :param text: input string
        :return: substituted string

        """
        # this is non-trivial https://stackoverflow.com/questions/10017147/removing-a-list-of-characters-in-string

        non_file_punct = '\t \n{}!@#$%^&*()[]:;\'",|\\~+=/`'
        # [unicode(x.strip()) if x is not None else '' for x in row]

        text0 = TextUtil.replace_chars(text, non_file_punct, "_")
        return text0

    @classmethod
    def get_suffix(cls, file):
        """get suffix of filename
        :param file: filename
        :return: suffix including the '.'

        """
        _suffix = None if file is None else Path(file).suffix
        return _suffix

    @staticmethod
    def check_exists(file):
        """
        raise exception on null value or non-existent path
        """
        if file is None:
            raise Exception("null path")

        if os.path.isdir(file):
            # logger.debug(path, "is directory")
            pass
        elif os.path.isfile(file):
            # logger.debug(path, "is path")
            pass
        else:
            try:
                f = open(file, "r")
                logger.debug("tried to open", file)
                f.close()
            except Exception:
                raise FileNotFoundError(str(file) + " should exist")

    @classmethod
    def copyanything(cls, src, dst, mkdir=True):
        """copy file or directory
        (from StackOverflow)
        :param src: source file/directory
        :param dst: destination
        :param mkdir: make dst directory if not exists
        """
        if src is None:
            raise ValueError("src is None")
        src = Path(src)
        if not src.exists():
            raise FileExistsError(f"src {src} does not exist")
        if dst is None:
            raise ValueError("dst is None")
        dst = Path(dst)
        if mkdir:
            if src.is_dir():
                FileLib.force_mkdir(dst)
            else:
                FileLib.force_mkdir(dst.parent)
        try:
            shutil.copytree(src, dst, dirs_exist_ok=mkdir)
        except OSError as exc:  # python >2.5
            if exc.errno in (errno.ENOTDIR, errno.EINVAL):
                shutil.copy(src, dst)
            else:
                raise exc

    @classmethod
    def copy_file(cls, file, src, dst, mkdir=True):
        """
        :param file: filename in src dir
        :param src: source directory
        :oaram dst: destination directory
        :param mkdir: if true force dst to exist
        """
        if file is None:
            logger.error(f"file is None")
            return None
        if src is None:
            logger.error(f"src is None")
            return None
        if dst is None:
            logger.error(f"dst is None")
            return None
        if not src.exists():
            logger.error(f"src {src} does not exist")
            return None
        if not Path(src, file) .exists():
            logger.error(f"file {file} does not exist")
            return None
        FileLib.force_mkdir(dst)


        FileLib.copyanything(Path(src, file), Path(dst, file))

    @classmethod
    def delete_directory_contents(cls, dirx, delete_directory=False):
        """
        deletes directories recursively using shutil.rmtree
        :param dirx: directory tree to delete
        :param delete_directory: If True, delete dirx
        :return None:
        """
        if not dirx or not Path(dirx).exists():
            print(f"no directory given or found {dirx}")
            return
        if delete_directory:
            shutil.rmtree(dirx)
        else:
            for path in Path(dirx).glob("**/*"):
                if path.is_file():
                    path.unlink()
                elif path.is_dir():
                    shutil.rmtree(path)

    @classmethod
    def delete_files(cls, dirx, globstr):
        """
        delete files in directory
        :param dirx: directory containing files
        :param globstr: glob string, e.g. "*.html"
        :return: list of deleted files (Paths)

        """
        files = []
        for path in Path(dirx).glob(globstr):
            if path.is_file():
                path.unlink()
                files.append(path)
        return files

    @classmethod
    def list_files(cls, dirx, globstr):
        """
        list files (not dirs) in directory
        :param dirx: directory containing files
        :param globstr: glob string, e.g. "*.html"
        :return: list of files (Paths)
        """
        return [path for path in Path(dirx).glob(globstr) if path.is_file()]

    @classmethod
    def size(cls, file):
        """
        get size of file
        :param file:
        :return: file size bytes else None if not exist
        """
        return None if file is None or not file.exists() else os.path.getsize(file)

    @classmethod
    def get_encoding(cls, file):
        """tries to guess (text) encoding
        :param file: to read
        :return: {'encoding': Guess, 'confidence': d.d, 'language': Lang}"""
        with open(file, "rb") as f:
            rawdata = f.read()
            return cls.get_encoding_from_bytes(rawdata)

    @classmethod
    def get_encoding_from_bytes(cls, rawdata):
        chardet.detect(rawdata)
        encode = chardet.UniversalDetector()
        encode.close()
        return encode.result

    @classmethod
    def expand_glob_list(cls, file_list):
        """
        :param file_list: list of paths including globs
        :return: flattened globbed list wwith posix names
        """
        if type(file_list) is not list:
            file_list = [file_list]
        files = []
        for file in file_list:
            globbed_files = FileLib.posix_glob(str(file))
            files.extend(globbed_files)
        return cls.convert_files_to_posix(files)

    @classmethod
    def convert_files_to_posix(cls, file_list):
        """converts list of files to posix form (i.e. all files have / not backslash)
        """
        if file_list is None:
            return None
        posix_files = [PurePosixPath(f) for f in file_list]
        return posix_files

    @classmethod
    def delete_file(cls, file):
        """delete file (uses unlink) and asserts it has worked
        ;param file: to delete"""
        file = Path(file)
        if file.exists():
            file.unlink()
        assert not file.exists()

    @classmethod
    def write_dict(cls, dikt, path, debug=False, indent=2):
        """write dictionary as JSON object
        :param dikt: python dictionary
        :param path: path to write to
        :param debug:
        :param indent:
        """

        with open(str(path), "w", encoding="UTF-8") as f:
            json.dump(dikt, f, indent=indent)
        if debug:
            logger.debug(f"wrote dictionary to {path}")

    @classmethod
    def read_string_with_user_agent(self, url, user_agent='my-app/0.0.1', encoding="UTF-8", encoding_scheme="chardet",
                                    debug=False):
        """
        allows request.get() to use a user_agent
        :param url: url to read
        :param encoding_scheme: id "chardet uses chardet else response.appenent_encoding
        :return: decoded string
        """
        if not url:
            return None
        if debug:
            logger.debug(f"reading {url}")
        response = requests.get(url, headers={'user-agent': user_agent})
        if debug:
            logger.debug(f"response: {response} content: {response.content[:400]}")
        content = response.content
        if debug:
            logger.debug(f"apparent encoding: {response.apparent_encoding}")
        if encoding is None:
            encoding = chardet.detect(content)[
                'encoding'] if encoding_scheme == "chardet" else response.apparent_encoding
        content = content.decode(encoding)
        return content, encoding

    @classmethod
    def join_dir_and_file_as_posix(cls, indir, input):
        """
        joins indir (directory) and input (descendants) to make a list of full filenames
        if indir or input is null, no action
        if indir is a list no action, returns input unchanged
        if input is absolute (starts with "/") no action

        if input is string, creates PosixPath(indir, input) VIA PosixPath
        if input is list of strings creates:
            f"{indir}/{input1}"
            f"{indir}/{input2}"
            ...
            it skips any input strings starting with "/"
        :param indir: input directory
        :param input: single file or list
        :return: single filke or files AS posix strings
        """
        if not indir or not input:
            return input
        # cannot manage multiple directories (?yet)
        if type(indir) is list and len(indir) > 1:
            return input

        if type(input) is str:
            # single input
            if input[0] != "/":
                output = PurePosixPath(indir, input)
                return str(output)
        elif type(input) is list:
            # list of inputs
            outputs = []
            for input_item in input:
                if input_item[0] != "/":
                    posix = PurePosixPath(indir, input_item)
                    outputs.append(str(posix))
            return outputs

    @classmethod
    def get_children(cls, parent, dirx=None, hidden=None):
        """
        convenience method
        gets some or all child files
        :param file: parent dir
        :param dirx:  True=include only directories, False=ignore directories, None=no action
        :param hidden:  True=include only hidden files, False=ignore hidden, None=no action (NYI)
        :return:list of files (may be empty)
        """
        files = []
        if parent is None or not Path(parent).is_dir():
            return files
        parent = Path(parent)
        files = list(parent.iterdir())
        if dirx is not None:
            files = [f for f in files if dirx == f.is_dir()]
        if hidden is not None:
            logger.warning(f"hidden option {hidden} is NYI")
        return files

    @classmethod
    def posix_glob(cls, glob_str, recursive=True):
        """expands glob and ensure all output is posix
        :param glob_str: glob or list of globs to expand
        :param recursive: use recursive glob
        :return: list of files in posix format"""
        files = []
        if glob_str is None:
            return files
        if type(glob_str) is str:
            glob_str = [glob_str]
        for globx in glob_str:
            ff = glob.glob(globx, recursive=recursive)
            files.extend(ff)
        files = FileLib.convert_files_to_posix(files)
        return files

    @classmethod
    def assert_exist_size(cls, file, minsize, abort=True, debug=True):
        """asserts a file exists and is of sufficient size
        :param file: file or path
        :param minsize: minimum size
        :param abort: throw exception if fails (not sure what this does)
        :param debug: output filename
        """
        path = Path(file)
        if debug:
            logger.debug(f"checking {file}")
        try:
            assert path.exists(), f"file {path} must exist"
            assert (s := path.stat().st_size) > minsize, f"file {file} size = {s} must be above {minsize}"
        except AssertionError as e:
            if abort:
                raise e

    @classmethod
    def get_home(cls):
        """
        gets home directory os.path.expanduser("~")
        """
        home = os.path.expanduser("~")
        return home

    # @classmethod
    # def get_logger_old(cls, filename, file_level=2, suffix=".py", level=logging.INFO):
    #     """creates logger for module, uses modulae name
    #     removes .py
    #     retains level of hierarchy
    #     e.g. foo/bar/junk.py with levels = 2 => bar.junk
    #     :param filename: to act as logger name
    #     :param file_level: to include in hierarchy
    #     :param level: logging level (default INFO)
    #     :param suffix: suffix to remove, e.g. ".py"
    #     """
    #     # logger.debug(f"============= file {__file__} name {__name__} =============")
    #     if filename:
    #         if filename[-len(suffix):] == suffix:
    #             filename = filename[:-len(suffix)]
    #         module = '.'.join(filename.split(os.path.sep)[-file_level:])
    #         logger = logging.getLogger(module)
    #
    #         # Create handlers for logging to the standard output and a file
    #         stdoutHandler = logging.StreamHandler(stream=sys.stdout)
    #         errHandler = logging.FileHandler("error.log")
    #
    #         # Set the log levels on the handlers
    #         stdoutHandler.setLevel(logging.DEBUG)
    #         errHandler.setLevel(logging.ERROR)
    #
    #         # Create a log format using Log Record attributes
    #         fmt = logging.Formatter(
    #             "%(name)s: %(asctime)s | %(levelname)s | %(filename)s:%(lineno)s | %(process)d >>> %(message)s"
    #         )
    #
    #         # Set the log format on each handler
    #         stdoutHandler.setFormatter(fmt)
    #         errHandler.setFormatter(fmt)
    #
    #         # Add each handler to the Logger object
    #         logger.addHandler(stdoutHandler)
    #         logger.addHandler(errHandler)
    #
    #         logger.info("Server started listening on port 8080")
    #         logger.warning(
    #             "Disk space on drive '/var/log' is running low. Consider freeing up space"
    #         )
    #
    #         try:
    #             raise Exception("Failed to connect to database: 'my_db'")
    #         except Exception as e:
    #             # exc_info=True ensures that a Traceback is included
    #             logger.error(e, exc_info=True)
    #
    #         logger.info(f"created logger {module} {logger}")
    #         return logger

    @classmethod
    def get_input_strings(cls, strings_in, split=False):
        """
        reads strings from a varietyb of inputs, e.g
        "foo"
        "foo bar" (with/out split)
        ["foo", "bar"]
        ["foo bar", "plugh xyzzy"] (with/out split)
        "/Users/my/file.txt"
        "https://not.yet/implemented"
        :param string_in: single word, list of words, or file/s with words (url not yet supported)
        :param split: splist all strings by spaces (default False)
        :return: list of strings or empty list
        """
        strings_out = []
        if strings_in is None or strings_in == []:
            return strings_out
        # ensure a list
        logger.debug(f"strings_in {type(strings_in)} .. {strings_in}")
        if type(strings_in) is str and strings_in[:1] == "[":
            strings_in = ast.literal_eval(strings_in)
        logger.debug(f"after literal eval strings_in {type(strings_in)} .. {strings_in}")
        strings_in = strings_in if type(strings_in) is list else [strings_in]
        for string_in in strings_in:
            # is it a file
            strings_read = cls.read_strings_from_path(string_in)
            if strings_read is None:
                # not a file
                strings_read = [string_in]
            if split:
                sss = []
                for string_read in strings_read:
                    sss.extend(string_read.strip().split())
                strings_read = sss
            strings_out.extend(strings_read)

        # else:
        #     strings_out = strings_in
        return strings_out

    @classmethod
    def read_strings_from_path(cls, file_with_strings, strip=True, ignore_blank=True):
        """
        reads a file with list of strings. Lines can be stripped and blank lines ignored
        :param file_with_strings: file with list of strimgs , one per line
        :return: list of strings (may be empty) or None (file not exists)
        """
        if file_with_strings is None:
            return None
        path_in = Path(file_with_strings)
        if not path_in.exists():
            return None
        lines = []
        with open(path_in, "r") as f:
            for line in f:
                if strip:
                    line = line.rstrip()
                if ignore_blank and line == "":
                    continue
                lines.append(line)
        return lines

    @classmethod
    def log_exception(self, e, logger):
        """
        formats exception message with hyperlinks and line_no
        :param e: exceptiom
        :param logger: standard logger
        """
        logger.error(e, exc_info=True)

    @classmethod
    def write_temp_html(cls, htmlx, temp_dir, temp_file="junk.html", pretty_print=True, debug=True):
        """
        writes a chunk of html to a temporary file

        """
        pptext = lxml.etree.tostring(htmlx, pretty_print=pretty_print)
        path = Path(temp_dir, temp_file)
        if path.exists():
            FileLib.delete_file(path)
        print(f"file exists {path.exists()}")
        # path.parent.mkdir(exist_ok=False, parents=False)
        print(f"file exists {path.exists()}")
        with open(str(path), "w", encoding="UTF-8") as f:
            f.write(pptext.decode())
        if debug:
            print(f"wrote {path}")

    @classmethod
    def read_counter_from_file(cls, counterpath):
        """
        read serialised counter
        """
        if Path(counterpath).exists():
            with open(counterpath, "r") as f:
                counter_string = f.read()
                return counter_string
        return None

    @classmethod
    def get_relative_path(cls, pathx, refpath, walk_up=True):
        """
        get path relative to refpath
        :param pathx: to relativise
        :param refpath: reference path
        :return: path to convert refpath to path
        :except: raise Value error if no relative path
        """
        if pathx is None:
            return None
        if refpath is None:
            return None
        # ensure we are working with Paths
        pathx = Path(pathx)
        refpath = Path(refpath)

        if walk_up:
            try:
                diffpath = pathx.relative_to(Path(refpath), walk_up=walk_up)
                return diffpath
            except TypeError as te:
                logger.info(f"walkup requires Python 3.12)")

        # without Python 3.12
        parts = pathx.parts
        refparts = refpath.parts
        for i, part_tuple in enumerate(zip(parts, refparts)):
            if part_tuple[0] != part_tuple[1]:
                break
        # add ".." for making reletive path
        new_parts = []
        for p in refparts[i:]:
            new_parts.append("..")
        for p in parts[i:]:
            new_parts.append(p)
        diffpath = PurePath(*new_parts)
        return diffpath

    @classmethod
    def normalize_to_posix(cls, path_str):
        """
        :param path_str: filename or part filename
        :return: posix-normalized filename
        """
        # If it contains backslashes, it's likely Windows-style
        if "\\" in path_str:
            return PureWindowsPath(path_str).as_posix()
        else:
            # Already POSIX-like, normalize using PurePosixPath to remove redundant slashes, etc.
            return PurePosixPath(path_str).as_posix()

    @classmethod
    def check_service_connection(cls, service_url, service_name=None, timeout=10, user_agent=None):
        """
        Check connection to a specified service with configurable parameters.
        
        Args:
            service_url (str): URL of the service to test
            service_name (str): Human-readable name of the service (for logging)
            timeout (int): Request timeout in seconds
            user_agent (str): Custom User-Agent string. If None, uses amilib default
        
        Returns:
            dict: Connection check results with keys:
                - 'connected': bool - True if connection successful
                - 'status_code': int - HTTP status code if available
                - 'response_time': float - Response time in seconds
                - 'error': str - Error message if connection failed
                - 'service': str - Service name tested
        """
        import time
        import requests
        from amilib.util import Util
        
        logger = Util.get_logger(__name__)
        
        # Use amilib default User-Agent if none specified
        if user_agent is None:
            user_agent = "Mozilla/5.0 (compatible; Pyami/0.1; +https://github.com/petermr/pyami/)"
        
        headers = {"User-Agent": user_agent}
        
        start_time = time.time()
        result = {
            'connected': False,
            'status_code': None,
            'response_time': None,
            'error': None,
            'service': service_name
        }
        
        try:
            logger.info(f"Checking connection to {service_name} at {service_url}")
            response = requests.get(service_url, headers=headers, timeout=timeout)
            
            result['status_code'] = response.status_code
            result['response_time'] = time.time() - start_time
            
            if response.status_code == 200:
                result['connected'] = True
                logger.info(f"Successfully connected to {service_name} (status: {response.status_code}, time: {result['response_time']:.2f}s)")
            else:
                result['error'] = f"HTTP {response.status_code}: {response.reason}"
                logger.warning(f"Connection to {service_name} failed: {result['error']}")
                
        except requests.exceptions.Timeout:
            result['error'] = f"Connection timeout after {timeout}s"
            logger.warning(f"Connection to {service_name} timed out")
        except requests.exceptions.ConnectionError as e:
            result['error'] = f"Connection error: {str(e)}"
            logger.warning(f"Connection to {service_name} failed: {result['error']}")
        except requests.exceptions.RequestException as e:
            result['error'] = f"Request error: {str(e)}"
            logger.warning(f"Connection to {service_name} failed: {result['error']}")
        except Exception as e:
            result['error'] = f"Unexpected error: {str(e)}"
            logger.error(f"Unexpected error checking {service_name}: {result['error']}")
        
        return result


    @classmethod
    def check_internet_connection(cls, timeout=5):
        """
        Check basic internet connectivity using Google's DNS.
        
        Args:
            timeout (int): Request timeout in seconds
        
        Returns:
            dict: Connection check results
        """
        return FileLib.check_service_connection(
            service_url=GOOGLE_SERVICE_URL,
            service_name=GOOGLE_SERVICE_NAME,
            timeout=timeout
        )

# see https://realpython.com/python-pathlib/

def main():
    logger.debug("started file_lib")
    # test_templates()

    logger.debug("finished file_lib")


if __name__ == "__main__":
    logger.debug("running file_lib main")
    main()
else:
    #    logger.debug("running file_lib main anyway")
    #    main()
    pass


# examples of regex for filenames


def glob_re(pattern, strings):
    return filter(re.compile(pattern).match, strings)


filenames = glob_re(r'.*(abc|123|a1b).*\.txt', os.listdir())
