"""I/O related helpers"""

import ast
import concurrent.futures
import email.utils
import email.mime.text
import math
import os
import shutil
import smtplib
import time
import typing

from pathlib import Path

import urllib.parse
import urllib3.exceptions

import requests
import lxml.etree as ET

import digiflow.common as dfc
import digiflow.digiflow_metadata as df_md

# please pylinter
# pylint:disable=c-extension-no-member

OAI_KWARG_FGROUP_IMG = "fgroup_images"
DEFAULT_FGROUP_IMG = "MAX"
OAI_KWARG_FGROUP_OCR = "fgroup_ocr"
DEFAULT_FGROUP_OCR = "FULLTEXT"
OAI_KWARG_POSTFUNC = "post_oai"
OAI_KWARG_REQUESTS = "request_kwargs"
REQUESTS_DEFAULT_TIMEOUT = 20
REQUEST_DEFAULT_PARALLEL_DOWNLOADS = 4

MIME_TYPE_JPG = "image/jpg"
MIME_TYPE_JPEG = "image/jpeg"

JPG_MIMES = [MIME_TYPE_JPG, MIME_TYPE_JPEG]


MIMETYPE_TO_EXTENSION = {
    "image/jpeg": ".jpg",
    "image/jpg": ".jpg",
    "image/png": ".png",
    "image/tiff": ".tif",
    "image/tif": ".tif",
    "application/pdf": ".pdf",
    "text/xml": ".xml",
    "application/xml": ".xml",
}


class LoadException(Exception):
    """Load of OAI Data failed"""


class ServerError(LoadException):
    """Loading of Record via OAI failed due
    response status_code indicating Server Error"""


class ClientError(LoadException):
    """Loading of Record via OAI failed due
    response status_code indicating Client Error"""


class ContentException(LoadException):
    """Loading of Record via OAI failed due unexpected
    returned content, indicating missing record data
    or even missing complete record"""


class OAIRecord:
    """
    OAI Record which may load XML metadata initially, then resources on-demand
    """

    def __init__(
        self,
        record_identifier: str,
        metadata_path: Path,
        loader: "OAILoader",
        mets_digital_object_identifier: typing.Optional[str] = None,
    ):
        self.record_identifier = record_identifier
        self.metadata_path = metadata_path
        self._loader = loader
        self._mets_reader: typing.Optional[df_md.MetsReader] = None
        self._mets_digital_object_identifier = mets_digital_object_identifier
        self._files: typing.Dict[str, typing.List[dfc.METSFile]] = {}

    @property
    def mets_reader(self) -> df_md.MetsReader:
        """Lazy initialize METS reader"""
        if self._mets_reader is None:
            self._mets_reader = df_md.MetsReader(
                self.metadata_path, self._mets_digital_object_identifier
            )
        return self._mets_reader

    def get_files(self, group: str) -> typing.List[dfc.METSFile]:
        """Get file information from METS for a specific group or all groups if None"""
        return self.mets_reader.get_files(use_group=group)

    def load_resources(
        self, group_names: typing.Optional[typing.List[str]], use_file_id: bool = False
    ) -> int:
        """
        Load resources on-demand

        Args:
            group_names: List of group names to load (e.g., ['images', 'ocr'])
                         If None, loads all configured groups
            use_file_id: Use METS file ID instead of URL token
        Returns:
            Number of resources loaded
        """
        loaded = 0
        dir_local = self.metadata_path.parent

        # Determine which resources to load
        for fgroup, mets_files in self._files.items():
            if group_names is not None and fgroup not in group_names:
                continue
            for mets_file in mets_files:
                res_url = mets_file.url
                url_final_token = res_url.split("/")[-1]
                if use_file_id:
                    url_final_token = mets_file.file_id
                local_path = self._loader.calculate_path(
                    dir_local,
                    mets_file.mimetype,
                    self._loader.key_images,
                    url_final_token,
                )
                if self._loader.handle_load(res_url, local_path, None):
                    loaded += 1
        return loaded


class OAILoader:
    """
    Load OAI Records with corresponding metadata

    optional: post-process metadata XML after download
              to change hrefs, drop unwanted fileGroups, ...
              (default: None, i.e. no actions)
    optional: resolve linked resources like images and ocr-data
              store resources in specified directory layout
              (defaults: 'MAX' for images, 'FULLTEXT' for ocr)
              additional request arguments
              (defaults: empty dict)
    """

    def __init__(self, base_url, **kwargs) -> None:
        self.base_url = base_url
        if not self.base_url.startswith("https://"):
            self.base_url = "https://" + self.base_url
        self.groups = {}
        self.post_oai = df_md.extract_mets
        self.request_kwargs = self._sanitize_kwargs(kwargs)
        if OAI_KWARG_POSTFUNC in kwargs:
            self.post_oai = kwargs[OAI_KWARG_POSTFUNC]
        self.key_images = (
            kwargs[OAI_KWARG_FGROUP_IMG]
            if OAI_KWARG_FGROUP_IMG in kwargs
            else DEFAULT_FGROUP_IMG
        )
        self.key_ocr = (
            kwargs[OAI_KWARG_FGROUP_OCR]
            if OAI_KWARG_FGROUP_OCR in kwargs
            else DEFAULT_FGROUP_OCR
        )
        self.groups[self.key_images] = []
        self.groups[self.key_ocr] = []
        self.mets_reader: typing.Optional[df_md.MetsReader] = None
        self.store: typing.Optional[LocalStore] = None
        self._session: typing.Optional[requests.Session] = None

    def _sanitize_kwargs(self, in_kwargs):
        top_dict = {}
        if OAI_KWARG_REQUESTS in in_kwargs:
            raw_kwargs = in_kwargs[OAI_KWARG_REQUESTS]
            if isinstance(raw_kwargs, dict):
                return raw_kwargs
            top_tokens = [r.strip() for r in raw_kwargs.split(",")]
            for t in top_tokens:
                k, v = t.split("=", maxsplit=1)
                try:
                    top_dict[k] = ast.literal_eval(v)
                except ValueError as val_err:
                    msg = f"Can't use {top_dict} to load: {val_err.args[0]}"
                    raise LoadException(msg) from val_err
        return top_dict

    def _get_session(self) -> requests.Session:
        """Get or create requests session for connection pooling"""
        if self._session is None:
            self._session = requests.Session()
        return self._session

    def __del__(self):
        """Cleanup session on destruction"""
        if self._session:
            self._session.close()

    def load(
        self,
        record_identifier,
        local_dst: Path,
        mets_digital_object_identifier=None,
        skip_resources=False,
        force_update=False,
        metadata_format="mets",
        use_file_id=False,
    ) -> int:
        """
        load metadata from OAI with optional caching in-between
        request additional linked resources if required

        * requires record_identifier to complete basic OAI request url
        * use mets_digital_object_identifier to get the proper MODS-section
          _if known_ for further inspection

        returns total of loaded metadata (1) plus number of
        additionally loaded resources
        """
        loaded = 0

        # sanitize url
        ctx = f"verb=GetRecord&metadataPrefix={metadata_format}&identifier={record_identifier}"
        res_url = f"{self.base_url}?{ctx}"
        if not isinstance(local_dst, Path):
            local_dst = Path(local_dst).absolute()
        assert local_dst is not None
        dir_local = local_dst.parent
        path_res = self.handle_load(res_url, local_dst, self.post_oai, force_update)
        if path_res:
            loaded += 1

        if skip_resources:
            return loaded

        # inspect if file resources shall be handled
        self.mets_reader = df_md.MetsReader(local_dst, mets_digital_object_identifier)

        # get linked resources
        for k in self.groups:
            self.groups[k] = self.mets_reader.get_files(use_group=k)

        # if exist, download them too
        post_func = None
        for k, file_entries in self.groups.items():
            if k == self.key_ocr:
                post_func = post_load_store_xml
            for mets_file in file_entries:
                assert isinstance(mets_file, dfc.METSFile)
                res_url = mets_file.url
                url_final_token = res_url.split("/")[-1]
                if use_file_id:
                    url_final_token = mets_file.file_id
                local_path = self.calculate_path(
                    dir_local, mets_file.mimetype, k, url_final_token
                )
                if self.handle_load(res_url, local_path, post_func):
                    loaded += 1
        return loaded

    def handle_load(self, res_url: str, res_path: Path, post_func, force_load=False):
        """Forward load request to store if configured, otherwise load directly"""
        if self.store:
            stored_path = self.store.get(res_path)
            # if in store found ...
            if stored_path:
                if not force_load:
                    return None
                # force update:
                # 1. rename existing data
                file_name = os.path.basename(stored_path)
                file_dir = os.path.dirname(stored_path)
                mets_ctime = str(int(os.stat(stored_path).st_mtime))
                bkp_mets = file_name.replace("mets", mets_ctime)
                os.rename(stored_path, os.path.join(file_dir, bkp_mets))
                # 2. download again anyway
                data_path = self.load_resource(res_url, res_path, post_func)
                if data_path:
                    self.store.put(data_path)
                return res_path
            data_path = self.load_resource(res_url, res_path, post_func)
            if data_path:
                self.store.put(data_path)
            return res_path
        return self.load_resource(res_url, res_path, post_func)

    def calculate_path(self, dir_local: Path, mimetype: str, *args):
        """
        calculate final path depending on used fileGrp
        'MAX' means images, not means 'xml'
        """
        res_path = os.path.join(dir_local, os.sep.join(args))
        if mimetype in JPG_MIMES and not res_path.endswith(".jpg"):
            res_path += ".jpg"
        if "/MAX/" in res_path and not res_path.endswith(".jpg"):
            res_path += ".jpg"
        if "xml" in mimetype and not res_path.endswith(".xml"):
            res_path += ".xml"
        if "/FULLTEXT/" in res_path and not res_path.endswith(".xml"):
            res_path += ".xml"
        return Path(res_path)

    def load_resource(self, url, path_local, post_func):
        """
        ensure local target dir exits and content can be written to
        optional: post-processing of received data if both exist
        """
        try:
            dst_dir = os.path.dirname(path_local)
            if not os.path.isdir(dst_dir):
                os.makedirs(dst_dir)
            local_path, data, content_type = request_resource(
                url, path_local, **self.request_kwargs
            )
            if post_func and data:
                # divide METS from XML (OCR-ALTO)
                # in rather brute force fashion
                data_snippet = data[:512]
                # propably sanitize data, as it might originate
                # from test-data or *real* requests
                if isinstance(data_snippet, bytes):
                    data_snippet = data_snippet.decode("utf-8")
                if dfc.XMLNS["mets"] in str(data_snippet) or dfc.XMLNS["oai"] in str(
                    data_snippet
                ):
                    data = post_func(path_local, data)
                elif "http://www.loc.gov/standards/alto" in str(data_snippet):
                    data = post_func(local_path, data)
                else:
                    raise LoadException(f"Can't handle {content_type} from {url}!")
            return local_path
        except df_md.DigiflowMetadataException as dfm_exc:
            raise LoadException(dfm_exc) from dfm_exc
        except LoadException as load_exc:
            raise load_exc
        except urllib3.exceptions.ReadTimeoutError as read_timeout:
            msg = f"load {url} read timeout: {read_timeout}"
            raise LoadException(msg) from read_timeout
        except Exception as exc:
            msg = f"load {url} exception: {exc}"
            raise RuntimeError(msg) from exc

    def clone(
        self,
        record_identifier: str,
        local_dst: Path,
        download_resources: bool = False,
        types: typing.Optional[typing.List[str]] = None,
        mets_digital_object_identifier: typing.Optional[str] = None,
        metadata_format: str = "mets",
        n_workers: int = REQUEST_DEFAULT_PARALLEL_DOWNLOADS,
        progress_callback: typing.Optional[typing.Callable] = None,
    ) -> typing.Dict[str, typing.Any]:
        """
        Clone an OAI record with comprehensive resource management.

        This method extends the basic load() functionality with:
        - Parallel resource downloading
        - Selective fileGroup filtering
        - Progress tracking and statistics

        Args:
            record_identifier: OAI identifier for the record
            local_dst: Local path for METS XML file
            download_resources: If True, download all linked resources
            types: List of fileGroup USE values to download (e.g., ['MAX', 'DOWNLOAD'])
                If None and download_resources=True, downloads all fileGroups
            mets_digital_object_identifier: Optional MODS identifier
            metadata_format: OAI metadata format (default: 'mets')
            n_workers: Number of parallel download workers (default: 4)
            progress_callback: Optional callback function(current, total, status_msg)

        Returns:
            Dictionary with statistics and information about the cloned record

        Examples:
            # Basic clone (METS only)
            stats = loader.clone('oai:repo:123456', Path('data/item1'))
            # Clone with all resources
            stats = loader.clone(
                'oai:repo:123456',
                Path('data/item2'),
                download_resources=True
            )

            # Clone only MAX images and PDFs
            stats = loader.clone(
                'oai:repo:123456',
                Path('data/item3'),
                download_resources=True,
                types=['MAX', 'DOWNLOAD']
            )
        """
        try:
            # Ensure local_dst is a Path
            if not isinstance(local_dst, Path):
                local_dst = Path(local_dst).absolute()
            output_dir = local_dst.parent
            # Step 1: Download METS record
            if progress_callback:
                progress_callback(0, 1, "Downloading METS record")
            ctx = f"verb=GetRecord&metadataPrefix={metadata_format}&identifier={record_identifier}"
            url = f"{self.base_url}?{ctx}"
            response = self._get_session().get(url, timeout=30, **self.request_kwargs)
            response.raise_for_status()
            # Ensure directory exists
            local_dst.parent.mkdir(parents=True, exist_ok=True)
            # Save METS - parse bytes to XML tree, extract METS, and write to file
            xml_root = ET.fromstring(response.content)
            mets_tree = df_md.post_oai_extract_metsdata(xml_root)
            assert mets_tree is not None
            df_md.write_xml_file(mets_tree, local_dst, preamble="")
            self.mets_reader = df_md.MetsReader(
                local_dst, mets_digital_object_identifier
            )
            if progress_callback:
                progress_callback(1, 1, "METS record downloaded")
            # Step 2: Download resources if requested
            if download_resources:
                try:
                    # Extract resources from METS based on fileGroup selection
                    if types:
                        # Selective download: Only fetch specified fileGroup types
                        # Example: types=['MAX', 'DOWNLOAD'] will only get MAX images and PDFs
                        all_resources = []
                        for file_type in types:
                            all_resources.extend(
                                self.mets_reader.get_files(use_group=file_type)
                            )
                        filtered_resources = all_resources
                    else:
                        # Complete download: Fetch all files from all available fileGroups
                        # This includes MAX, THUMBS, DEFAULT, DOWNLOAD, FULLTEXT, etc.
                        # Note: Must iterate through _files.keys() since get_files() defaults
                        # to 'MAX' only when called without arguments
                        all_resources = []
                        for use_group in self.mets_reader._files.keys():
                            all_resources.extend(
                                self.mets_reader.get_files(use_group=use_group)
                            )
                        filtered_resources = all_resources
                    # Download in parallel
                    results = self._download_resources_parallel(
                        filtered_resources,
                        output_dir,
                        max_workers=n_workers,
                        progress_callback=progress_callback,
                    )
                    return results
                except Exception as e:
                    error_msg = f"Resource download error: {e}"
        except Exception as e:
            error_msg = f"Clone operation failed: {e}"
            raise LoadException(error_msg) from e
        return {}

    def _download_resources_parallel(
        self,
        resources: typing.List[dfc.METSFile],
        output_dir: Path,
        max_workers: int = 5,
        progress_callback: typing.Optional[typing.Callable] = None,
    ) -> dict:
        """
        Download resources using parallel workers.

        Args:
            resources: Dictionary of resources grouped by type
            output_dir: Base output directory
            max_workers: Number of parallel download workers
            progress_callback: Optional callback(current, total, status_msg)

        Returns:
            Statistics dictionary
        """
        ts_start = time.time()
        stats = {
            "total": 0,
            "success": 0,
            "failed": 0,
            "size_bytes": 0,
            "by_type": {},
            "duration_seconds": 0,
            "n_workers": max_workers,
        }
        # Flatten resources list
        if not resources:
            return stats
        stats["total"] = len(resources)
        session = self._get_session()
        # Download with parallel workers
        with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
            # Submit all download tasks
            future_to_resource = {
                executor.submit(
                    self._download_single_resource, resource, output_dir, session
                ): resource
                for resource in resources
            }
            # Process completed downloads
            for i, future in enumerate(
                concurrent.futures.as_completed(future_to_resource), 1
            ):
                result = future.result()
                if result["success"]:
                    stats["success"] += 1
                    stats["size_bytes"] += result["size"]
                    use_type = result["use"]
                    stats["by_type"][use_type] = stats["by_type"].get(use_type, 0) + 1
                    if progress_callback:
                        progress_callback(
                            i, stats["total"], f"Downloaded {result['file']}"
                        )
                else:
                    stats["failed"] += 1
                    if progress_callback:
                        progress_callback(i, stats["total"], f"Failed {result['file']}")
            ts_duration = math.floor(time.time() - ts_start)
            stats["duration_seconds"] = ts_duration
        return stats

    def _download_single_resource(
        self, resource: dfc.METSFile, output_dir: Path, session: requests.Session
    ) -> dict:
        """
        Download a single resource.
        Args:
            resource: Resource information dictionary
            output_dir: Base output directory
            session: Requests session for connection pooling
        Returns:
            Result dictionary with success status and metadata
        """
        try:
            url = resource.url
            file_id = resource.file_id
            use = resource.use
            path_segment = urllib.parse.urlparse(url).path
            local_path = output_dir / use / path_segment.rsplit("/", maxsplit=1)[-1]
            output_path = _sanitize_local_file_extension(
                local_path, resource.mimetype, file_id
            )
            # Create directory if it doesn't exist
            output_path.parent.mkdir(parents=True, exist_ok=True)
            # Download file with timeout from request_kwargs or default
            timeout = self.request_kwargs.get("timeout", 30)
            response = session.get(url, timeout=timeout, **self.request_kwargs)
            response.raise_for_status()
            # Save file
            with open(output_path, "wb") as f:
                f.write(response.content)
            return {
                "success": True,
                "file": output_path.name,
                "use": use,
                "size": len(response.content),
                "path": output_path,
            }
        except (OSError, requests.RequestException) as e:
            return {
                "success": False,
                "file": resource.file_id,
                "use": resource.use,
                "error": str(e),
            }


class OAIFileSweeper:
    """delete all resources (files and empty folder),
    except colorchecker, if any
    parse *.mets.xml to identify files to be deleted
    """

    def __init__(self, path_store, pattern="mets.xml", filegroups=None):
        self.work_dir = path_store
        self.pattern = pattern
        self.filegroups = filegroups if isinstance(filegroups, list) else ["MAX"]

    def sweep(self):
        """remove OAI-Resources from given dir, if any contained"""

        work_dir = self.work_dir
        total = 0
        size = 0
        for curr_root, dirs, files in os.walk(work_dir):
            for filegroup in self.filegroups:
                if filegroup not in dirs:
                    continue
                curr_root_path = Path(curr_root)
                curr_filegroup_folder = curr_root_path / filegroup
                _files = [f for f in curr_filegroup_folder.iterdir() if f.is_file()]

                if filegroup == "MAX" and len(_files) < 2:
                    # One image only? This is likely the colorchecker! --> skip legacy

                    continue
                curr_mets_files = [xml for xml in files if xml.endswith(self.pattern)]
                if curr_mets_files:
                    curr_mets_file = curr_mets_files[0]
                    curr_mets = curr_root_path.joinpath(curr_mets_file)
                    files_to_del = self._get_files(curr_mets, filegroup)

                    for pth in _files:
                        if pth.stem in files_to_del:
                            total += 1
                            size += pth.stat().st_size
                            try:
                                pth.unlink()
                                _parent = pth.parent
                                if (
                                    _parent.is_dir()
                                    and len(list(_parent.iterdir())) == 0
                                ):
                                    _parent.rmdir()
                            except PermissionError:
                                return f"cannot delete {pth} due insuff. perm."
        return (work_dir, total, f"{(size >> 20)} Mb")

    def _get_files(self, mets_xml, filegroup):
        xml_root = ET.parse(str(mets_xml)).getroot()
        xpath = f".//mets:fileGrp[@USE='{filegroup}']/mets:file/mets:FLocat"
        locats = xml_root.findall(xpath, {"mets": "http://www.loc.gov/METS/"})
        links = [xl.get("{http://www.w3.org/1999/xlink}href") for xl in locats]
        return [Path(ln).stem for ln in links]


class LocalStore:
    """cache physical resources"""

    def __init__(self, dir_store_root, dir_local):
        self.dir_store_root = dir_store_root
        self.dir_local = dir_local

    def _calculate_path(self, path_res):
        if not isinstance(path_res, str):
            path_res = str(path_res)
        sub_path_res = path_res.replace(str(self.dir_local), "")
        if sub_path_res.startswith("/"):
            sub_path_res = sub_path_res[1:]
            if sub_path_res:
                return os.path.join(str(self.dir_store_root), sub_path_res)
        return None

    def get(self, path_res):
        """
        push resources by path into dst_dir, if
        they exist in store:
         * if single file requested, restore single file
         * if directory requested, restore from directory
        """

        path_store = self._calculate_path(path_res)
        if path_store and os.path.exists(path_store):
            dst_dir = os.path.dirname(path_res)
            if not os.path.exists(dst_dir):
                os.makedirs(dst_dir, exist_ok=True)
            if os.path.isfile(path_store):
                shutil.copy2(path_store, path_res)
            elif os.path.isdir(path_store):
                shutil.copytree(path_store, path_res)
            return path_store
        return None

    def put(self, path_res):
        """put single resource to path, for example created ocr data"""

        path_store = self._calculate_path(path_res)
        assert path_store is not None
        path_local_dir = os.path.dirname(path_store)
        if not os.path.exists(path_local_dir):
            os.makedirs(path_local_dir)
        # type cast to str required by python 3.5
        shutil.copy2(str(path_res), path_store)
        return path_store

    def put_all(self, src_dir, filter_ext=".xml"):
        """
        put all resources singular from last directory
        segement of current source directory to dst dir,
        if matching certain file_ext (default: "*.xml")
        """

        last_dir = os.path.basename(src_dir)
        dst_dir = os.path.join(self.dir_store_root, last_dir)
        if not os.path.exists(dst_dir):
            os.makedirs(dst_dir)
        entries = os.listdir(src_dir)
        existing = [f for f in entries if str(f).endswith(filter_ext)]
        stored = 0
        for ent in existing:
            src = os.path.join(src_dir, ent)
            dst = os.path.join(dst_dir, ent)
            shutil.copy(src, dst)
            stored += 1
        return stored


def request_resource(url: str, path_local: Path, **kwargs):
    """
    request resources from provided url
    * textual content is interpreted as xml and
      passed back as string for further processing
    * binary image/jpeg content is stored at path_local
      passes back path_local
    * optional params (headers, cookies, ... ) forwarded as kwargs

    Raises Exception for unknown Content-Types and requests' Errors
    (https://docs.python-requests.org/en/master/_modules/requests/exceptions/).

    Returns Tuple (response_status, result)
    """

    status = 0
    result = None
    try:
        the_timeout = REQUESTS_DEFAULT_TIMEOUT
        if "timeout" in kwargs:
            the_timeout = kwargs["timeout"]
            del kwargs["timeout"]
        response = requests.get(url, timeout=the_timeout, **kwargs)
        status = response.status_code
        if status >= 400:
            the_info = f"{url} status {status}"
            if status < 500:
                raise ClientError(the_info)
            raise ServerError(the_info)
        content_type = None
        if status == 200:
            content_type = response.headers["Content-Type"]
            # textual xml data
            if "text" in content_type or "xml" in content_type:
                result = response.content
            # catch other content types by MIMI sub_type
            # split "<application|image>/<sub_type>"
            elif content_type.split("/")[-1] in ["jpg", "jpeg", "pdf", "png"]:
                path_local = _sanitize_local_file_extension(path_local, content_type)
                if not isinstance(path_local, Path):
                    path_local = Path(path_local)
                path_local.write_bytes(response.content)
            # if we went this far, something unexpected has been returned
            else:
                msg = f"download {url} with unhandled content-type {content_type}"
                raise ContentException(msg)
        return (path_local, result, content_type)
    except OSError as exc:
        msg = f"fail to download {url} to {path_local}"
        raise RuntimeError(msg) from exc


def _sanitize_local_file_extension(
    path_local: Path, content_type: str, file_id: typing.Optional[str] = None
) -> Path:
    if not isinstance(path_local, Path):
        path_local = Path(path_local).absolute()
    correct_ext = MIMETYPE_TO_EXTENSION.get(content_type)
    if not correct_ext:
        for mime, ext in MIMETYPE_TO_EXTENSION.items():
            if mime.split("/", maxsplit=1)[-1] in content_type:
                correct_ext = ext
                break
    file_name = path_local.stem
    if file_name == 'default' and file_id:
        file_name = file_id
    path_local = path_local.with_name(file_name)
    if correct_ext and path_local.suffix != correct_ext:
        path_local = path_local.with_suffix(correct_ext)
    return path_local


def smtp_note(smtp_conn: str, subject: str, message: str, froms: str, tos):
    """Notify recipients about subject
    in true local time
    stmp_connn: str Information about host:port
    subject: str The Subject
    message: str The Message
    sender: str Email of sender
    recipients: str, list  Email(s)
    """

    if isinstance(tos, list):
        tos = ",".join(tos)
    try:
        msg = email.mime.text.MIMEText(message)
        msg["Date"] = email.utils.formatdate(localtime=True)
        msg["Subject"] = subject
        msg["From"] = froms
        msg["To"] = tos + "\n"
        server = smtplib.SMTP(smtp_conn)
        server.send_message(msg)
        server.quit()
        return f"'{tos}' note: '{message}'"
    except Exception as _exc:
        return f"Failed to notify '{tos}': '{_exc.args[0]}' ('{message}')!"


def post_load_store_xml(path_local, the_data):
    """
    Store XML as it is - encoding required with OCR-strings but not
    for byte objects like from semantics fulltext responses
    """

    if isinstance(the_data, str):
        the_data = the_data.encode("utf-8")
    xml_root = ET.fromstring(the_data)
    df_md.write_xml_file(xml_root, path_local, preamble="")


@dfc.deprecated(
    "This function has limited use cases and will be removed in future versions"
)
def get_enclosed(
    tokens_str: str, mark_end="}", mark_start="{", func_find="rfind"
) -> str:
    """
    Search dict-like enclosed entry in string
    from end (rfind) or start (find)

    If no match, return empty string ''

    .. deprecated::
        This function has limited use cases and will be removed in future versions
    """
    if mark_end in tokens_str and mark_start in tokens_str:
        _offset_right_end = tokens_str.__getattribute__(func_find)(mark_end)
        _offset_right_start = tokens_str[:_offset_right_end].__getattribute__(
            func_find
        )(mark_start)
        _the_enclosed = tokens_str[_offset_right_start : (_offset_right_end + 1)]
        return _the_enclosed
    return ""
