import fnmatch
import json
import logging
import os
import re
import shutil

import borgmatic.actions.restore

logger = logging.getLogger(__name__)

IS_A_HOOK = False


def make_data_source_dump_path(borgmatic_runtime_directory, data_source_hook_name):
    '''
    Given a borgmatic runtime directory and a data source hook name, construct a data source dump
    path.
    '''
    return os.path.join(borgmatic_runtime_directory, data_source_hook_name)


def make_data_source_dump_filename(
    dump_path, name, hostname=None, port=None, container=None, label=None
):
    '''
    Based on the given dump directory path, data source name, hostname, and port, return a filename
    to use for the data source dump. The hostname defaults to localhost.

    Raise ValueError if the data source name is invalid.
    '''
    if os.path.sep in name:
        raise ValueError(f'Invalid data source name {name}')

    if container:
        hostname = container
    elif not hostname:
        hostname = 'localhost'

    identifier = label if label else hostname + ('' if port is None else f':{port}')

    return os.path.join(dump_path, identifier, name)


def write_data_source_dumps_metadata(borgmatic_runtime_directory, hook_name, dumps_metadata):
    '''
    Given the borgmatic runtime directory, a data source hook name, and a sequence of
    borgmatic.actions.restore.Dump instances of dump metadata, write a metadata file describing all
    of those dumps. This metadata is being dumped so that it's available upon restore, e.g. to
    support the user selecting which data source(s) should be restored.

    Raise ValueError if writing to the file results in an operating system error.
    '''
    dumps_metadata_path = os.path.join(borgmatic_runtime_directory, hook_name, 'dumps.json')

    try:
        with open(dumps_metadata_path, 'w', encoding='utf-8') as metadata_file:
            json.dump(
                {
                    'dumps': [dump._asdict() for dump in dumps_metadata],
                },
                metadata_file,
                sort_keys=True,
            )
    except OSError as error:
        raise ValueError(f'Error writing to dumps metadata at {dumps_metadata_path}: {error}')


def parse_data_source_dumps_metadata(dumps_json, dumps_metadata_path):
    '''
    Given a dumps metadata JSON string as extracted from an archive and its path within the archive,
    parse it into a tuple of borgmatic.actions.restore.Dump instances and return them.

    Raise ValueError if parsing the JSON results in a JSON decode error or the data does not have
    the expected keys.
    '''
    try:
        return tuple(
            borgmatic.actions.restore.Dump(**dump) for dump in json.loads(dumps_json)['dumps']
        )
    except (json.JSONDecodeError, TypeError, KeyError) as error:
        raise ValueError(
            f'Cannot read archive data source dumps metadata at {dumps_metadata_path} due to invalid JSON: {error}',
        )


def create_parent_directory_for_dump(dump_path):
    '''
    Create a directory to contain the given dump path.
    '''
    os.makedirs(os.path.dirname(dump_path), mode=0o700, exist_ok=True)


def create_named_pipe_for_dump(dump_path):
    '''
    Create a named pipe at the given dump path.
    '''
    create_parent_directory_for_dump(dump_path)
    os.mkfifo(dump_path, mode=0o600)


def remove_data_source_dumps(dump_path, data_source_type_name, dry_run):
    '''
    Remove all data source dumps in the given dump directory path (including the directory itself).
    If this is a dry run, then don't actually remove anything.
    '''
    dry_run_label = ' (dry run; not actually removing anything)' if dry_run else ''

    logger.debug(f'Removing {data_source_type_name} data source dumps{dry_run_label}')

    if dry_run:
        return

    if os.path.exists(dump_path):
        shutil.rmtree(dump_path)


END_OF_STRING_PATTTERN = re.compile(r'\\z', flags=re.IGNORECASE)


def convert_glob_patterns_to_borg_pattern(patterns):
    '''
    Convert a sequence of shell glob patterns like "/etc/*", "/tmp/*" to the corresponding Borg
    regular expression archive pattern as a single string like "re:etc/.*|tmp/.*".
    '''
    # This deserves some explanation. Here's what this code is doing for each shell glob pattern:
    #
    #  * Strip off the leading slash, since Borg doesn't store leading slashes in archives.
    #  * Use fnmatch.translate() to convert the pattern to a Borg pattern.
    #  * Remove the "\z" or \"Z" end-of-string special character generated by fnmatch.translate(),
    #    because Borg doesn't like it. Replace it with a "$" end-of-string special character
    #    instead. And yes, this is using a regular expression to modify a regular expression.
    #  * Do the above for each of:
    #    * The plain pattern. This supports the use case of a standard database where its name is a
    #      filename in the path. Example: borgmatic/foo_databases/hostname/test
    #    * The pattern with "/*" tacked onto the end of it. This supports the use case of a
    #      directory-format database where its name is a directory name in the path. Example:
    #      borgmatic/foo_databases/hostname/test/toc.dat
    #
    # Join the resulting transformed patterns together with "|" and return them as a string.
    return 're:' + '|'.join(
        re.sub(END_OF_STRING_PATTTERN, '$', fnmatch.translate(stripped))
        + '|'
        + re.sub(END_OF_STRING_PATTTERN, '$', fnmatch.translate(stripped + '/*'))
        for pattern in patterns
        for stripped in (pattern.lstrip('/'),)
    )
