import configparser
import datetime
import os
import os.path as path
import socket
import subprocess
from configparser import ConfigParser

import utils.errors as errs
import utils.subprocess_utils as sub_utils
from utils.config_parse_helper import ConfigSchema, ConfigSchemaSection, ConfigSchemaLine, ParsedConfig, SpecialSchemaProcessing

DEFAULT_SCHEMA_SECTION = ConfigSchemaSection('DEFAULT',
    [ ConfigSchemaLine('backup_paths', True, list, SpecialSchemaProcessing.commma_separated)
    , ConfigSchemaLine('source_machine', True, str)
    , ConfigSchemaLine('verbose', False, bool)
    , ConfigSchemaLine('make_dirs', False, bool)
    ]
)
COPY_SCHEMA_SECTION = ConfigSchemaSection('COPY',
    [ ConfigSchemaLine('sources', True, list, SpecialSchemaProcessing.commma_separated)
    , ConfigSchemaLine('additional_args', False, list, SpecialSchemaProcessing.space_separated)
    ]
)
TAR_SCHEMA_SECTION = ConfigSchemaSection('TAR',
    [ ConfigSchemaLine('sources', True, list, SpecialSchemaProcessing.commma_separated)
    , ConfigSchemaLine('additional_args', False, list, SpecialSchemaProcessing.space_separated)
    , ConfigSchemaLine('target_filename', True, str)
    , ConfigSchemaLine('prefix_date', True, bool)
    ]
)
RSYNC_SCHEMA_SECTION = ConfigSchemaSection('RSYNC',
    [ ConfigSchemaLine('sources', True, list, SpecialSchemaProcessing.commma_separated)
    , ConfigSchemaLine('additional_args', False, list, SpecialSchemaProcessing.space_separated)
    , ConfigSchemaLine('sub-dirs', False, list, SpecialSchemaProcessing.commma_separated)
    ]
)

MYSQLDUMP_SCHEMA_SECTION = ConfigSchemaSection('MYSQLDUMP',
    [ ConfigSchemaLine('user', True, str)
    , ConfigSchemaLine('password', True, str)
    , ConfigSchemaLine('db_name', True, str)
    , ConfigSchemaLine('additional_args', False, list, SpecialSchemaProcessing.space_separated)
    , ConfigSchemaLine('target_filename', True, str)
    , ConfigSchemaLine('prefix_date', True, bool)
    ]
)


def get_backup_schema() -> ConfigSchema:
    backup_schema = ConfigSchema(None)
    backup_schema.add_section(DEFAULT_SCHEMA_SECTION)
    backup_schema.add_section(COPY_SCHEMA_SECTION)
    backup_schema.add_section(TAR_SCHEMA_SECTION)
    backup_schema.add_section(RSYNC_SCHEMA_SECTION)
    backup_schema.add_section(MYSQLDUMP_SCHEMA_SECTION)
    return backup_schema


class Backup():
    """Base class for creating backups, contains all the generic versions of
functions for copying, taring etc.
    """

    def __init__(self, config_file=None, schema: ConfigSchema=None):
        if config_file is None:
            raise errs.ValidationError("ValidationError: Backup class must be given a config")
        if schema is None:
            schema = get_backup_schema()
        self.load_config(config_file, schema)

    def load_config(self, config_class_or_filepath, schema):
        """Accepts a filepath to a config file, or """

        config = ConfigParser()
        expected_class = configparser.ConfigParser
        if config_class_or_filepath is None:
            raise errs.ValidationError("ValidationError: config settings or filepath to config must be given")
        elif isinstance(config_class_or_filepath, expected_class):
            config = config_class_or_filepath
        elif not path.exists(config_class_or_filepath):
            raise errs.ValidationError(f"ValidationError: Config filepath does not exist (or incorrect class given, expected: {expected_class})")
        config.read(config_class_or_filepath)

        self.read_config_and_schema(config, schema)

    def read_config_and_schema(self, config: ConfigParser, schema: ConfigSchema):
        parsed_config = ParsedConfig(config, schema)
        self.default_dict = parsed_config.get_section('DEFAULT')
        self.hostname_is_valid()
        self.maybe_make_backup_dirs()

        section = 'COPY'
        self.should_run_copy = parsed_config.has_section(section)
        if self.should_run_copy:
            self.copy_dict = parsed_config.get_section(section)

        section = 'TAR'
        self.should_run_tar = parsed_config.has_section(section)
        if self.should_run_tar:
            self.tar_dict = parsed_config.get_section(section)

        section = 'RSYNC'
        self.should_run_rsync = parsed_config.has_section(section)
        if self.should_run_rsync:
            self.rsync_dict = parsed_config.get_section(section)

        section = 'MYSQLDUMP'
        self.should_run_mysqldump = parsed_config.has_section(section)
        if self.should_run_mysqldump:
            self.mysqldump_dict = parsed_config.get_section(section)

    def run_backup_processes(self):
        self._write_begin_log()
        if self.should_run_copy:
            self._run_copy()
        if self.should_run_tar:
            self._run_tar()
        if self.should_run_rsync:
            self._run_rsync()
        if self.should_run_mysqldump:
            self._run_mysqldump()
        self._write_end_log()

    def hostname_is_valid(self) -> bool:
        if self.default_dict is None or 'source_machine' not in self.default_dict.keys():
            raise ValueError('No source machine specified')

        actual_hostname = socket.gethostname()
        return actual_hostname == self.default_dict['source_machine']

    def target_backup_paths_ok(self, raise_error=False):
        for bp in self.default_dict['backup_paths']:
            if not path.exists(bp):
                if raise_error:
                    raise errs.ValidationError(f"ValidationError: target path does not exist: {bp}")
                return False
        return True

    def maybe_make_backup_dirs(self):
        if self.default_dict is None or 'make_dirs' not in self.default_dict.keys():
            return
        if self.default_dict['make_dirs']:
            for backup_path in self.default_dict['backup_paths']:
                if not os.path.exists(backup_path):
                    os.makedirs(backup_path)
                    sub_utils.chmod_wrapper(['777', backup_path])

    def _run_copy(self):
        if self.should_run_copy is None or False:
            return 0
        self.target_backup_paths_ok(raise_error=True)

        for bp in self.default_dict['backup_paths']:
            # if not path.exists(bp):
            #         raise errs.ValidationError(f"ValidationError: target path does not exist: {bp}")

            for s in self.copy_dict['sources']:
                if not path.exists(s):
                    raise errs.ValidationError(f"ValidationError: target path does not exist: {s}")
                try:
                    args = [s, bp]
                    if 'additional_args' in self.copy_dict.keys():
                        args = args + self.copy_dict['additional_args']
                    sub_utils.cp_wrapper(args)
                except errs.SubprocessError as e:
                    raise errs.SubprocessError( f"Error: copying\n"
                                                f"    from: {s}\n"
                                                f"    to: {bp}\n"
                                              ) from e
        return 0

    def _run_tar(self):
        if self.should_run_tar is None or False:
            return 0

        backup_paths = self.default_dict['backup_paths']

        filename = self.tar_dict['target_filename']
        if 'prefix_date' in self.tar_dict.keys() and self.tar_dict['prefix_date']:
            filename = f"{get_date()}_{filename}"
        filename = path.join(backup_paths[0], filename)  # may need some tar suffix magic

        add_args = [] if 'additional_args' not in self.tar_dict.keys() else self.tar_dict['additional_args']
        all_args = add_args + [filename] + self.tar_dict['sources']

        try:
            sub_utils.tar_wrapper(all_args)
            sub_utils.chmod_wrapper(['774', filename])
            # copy to backups
            if len(backup_paths) == 1:
                return 0
            for d in range(1, len(backup_paths)):
                sub_utils.cp_wrapper([ filename, backup_paths[d]])
        except errs.SubprocessError as e:
            raise errs.SubprocessError() from e
        return 0

    def _run_rsync(self):
        if self.should_run_rsync is None or False:
            return 0

        self.target_backup_paths_ok(raise_error=True)
        backup_paths = self.default_dict['backup_paths']

        sources = self.rsync_dict['sources']
        for i in range(0, len(sources)):
            source = sources[i]
            if not path.exists(source):
                    raise errs.ValidationError(f"ValidationError: target path does not exist: {source}")

            for bp in backup_paths:
                try:
                    additional_args = []
                    if 'additional_args' in self.rsync_dict.keys():
                        additional_args = self.rsync_dict['additional_args']

                    source_and_target_args = [source, bp]

                    # sub-dirs option allows items to be backed up to subdirs within
                    # the source for better naming
                    if 'sub-dirs' in self.rsync_dict:
                        sub_dirs = self.rsync_dict['sub-dirs']
                        if len(sub_dirs) != len(sources):
                            raise errs.ValidationError(f"ValidationError: if using " +
                                "subdirs the number of subdirs must equal the number of sources")
                        bp_with_subdir = os.path.join(bp, sub_dirs[i])
                        if not os.path.exists(bp_with_subdir):
                            os.makedirs(bp_with_subdir)
                        source_and_target_args = [source, bp_with_subdir]

                    args = source_and_target_args + additional_args
                    sub_utils.local_rsync_wrapper(args)

                except errs.SubprocessError as e:
                    raise errs.SubprocessError( f"Error: rsync\n"
                                                f"    from: {source_and_target_args[0]}\n"
                                                f"    to: {source_and_target_args[1]}\n"
                                              ) from e
        return 0

    def _run_mysqldump(self):

        backup_paths = self.default_dict['backup_paths']

        mysqldump_args = [ 'mysqldump'
                         , '-u', self.mysqldump_dict['user']
                         , f"-p{self.mysqldump_dict['password']}"
                         , '--databases', self.mysqldump_dict['db_name']
                         ]
        if 'additional_args' in self.mysqldump_dict.keys():
            mysqldump_args = mysqldump_args + self.mysqldump_dict['additional_args']

        dump_filename = self.mysqldump_dict['target_filename']
        if 'prefix_date' in self.mysqldump_dict.keys() and self.mysqldump_dict['prefix_date']:
            dump_filename = f"{get_date()}_{dump_filename}"
        dump_filename = path.join(backup_paths[0], dump_filename)

        if path.exists(dump_filename):
            raise errs.ValidationError(f"ValidationError: mysqldump already exists {dump_filename}")

        with open(dump_filename, "w+") as df:
            mysqldump_process = subprocess.Popen(mysqldump_args, stdout=df, stderr=subprocess.PIPE)
            dump_result = mysqldump_process.communicate()

        # Get the piped std err. See if it contains the word error
        stdo, stde = dump_result
        if stde is not None:
            decoded_stderr = stde.decode()
            if decoded_stderr.casefold().find('error'.casefold()) > -1:
                raise errs.SubprocessError(f"SubprocessError: Errors detected in mysqldump:\n{decoded_stderr}")

        # copy the dump to other locations
        for d in range(1, len(backup_paths)):
            sub_utils.cp_wrapper([dump_filename, backup_paths[d]])
        return 0

    def _write_begin_log(self):
        backup_paths = self.default_dict["backup_paths"]
        for bp in backup_paths:
            log_file = path.join(bp, "latest_run.log")
            if path.exists(log_file):
                os.remove(log_file)
            with open(log_file, "w+") as lf:
                lf.write(f'Run started at: {datetime.datetime.now().strftime("%Y_%b_%d_%H:%M")}\n')

    def _write_end_log(self):
        backup_paths = self.default_dict["backup_paths"]
        for bp in backup_paths:
            log_file = path.join(bp, "latest_run.log")
            if not path.exists(log_file):
                return
            with open(log_file, "a") as lf:
                lf.write(f'Last successful backup completed at : {datetime.datetime.now().strftime("%Y_%b_%d_%H:%M")}\n')


def get_date(custom_date=None, custom_format="%Y_%m_%d"):
        # Format is currently: 2019-10-08
        if custom_date is not None:
            return custom_date.strftime(custom_format)
        return datetime.date.today().strftime(custom_format)
