import argparse
import json
import logging
import logging.config
import sys

from ftp_migrator import messages
from ftp_migrator.ftp import Ftp
from ftp_migrator.issue import Issue
from ftp_migrator.tools import safe_format, safe_string_repr

logger = logging.getLogger('plesk')


class Config(object):
    """Configuration for the migration"""
    def __init__(
        self,
        local_path=None, remote_path=None, server=None, user=None, password=None,
        use_ftps=None, excludes=None, encoding=None, ignore_passive_mode_server_ip=None
    ):
        """Class constructor

        :type local_path: str | unicode | None
        :type remote_path: str | unicode | None
        :type server: str | unicode | None
        :type user: str | unicode | None
        :type password: str | unicode | None
        :type use_ftps: bool | None
        :type excludes: list | None
        :type encoding: str | unicode | None
        :type ignore_passive_mode_server_ip: bool | None
        """
        self._local_path = local_path
        self._remote_path = remote_path
        self._server = server
        self._user = user
        self._password = password
        self._use_ftps = use_ftps
        self._excludes = excludes
        self._encoding = encoding
        self._ignore_passive_mode_server_ip = ignore_passive_mode_server_ip

    @property
    def local_path(self):
        """Where to copy on local server

        The path is a binary string in UTF-8 encoding.

        :rtype: str | None
        """
        return self._local_path

    @property
    def remote_path(self):
        """Where from copy on remote server

        The path is a binary string in UTF-8 encoding.

        :rtype: str | None
        """
        return self._remote_path

    @property
    def server(self):
        """Remote server's hostname or IP address

        :rtype: str | unicode | None
        """
        return self._server

    @property
    def user(self):
        """FTP user login

        :rtype: str | unicode | None
        """
        return self._user

    @property
    def password(self):
        """FTP user password

        :rtype: str | unicode | None
        """
        return self._password

    @property
    def use_ftps(self):
        """Whether to use FTPS (FTP + TLS/SSL) instead of plain insecure FTP

        :rtype: bool | None
        """
        return self._use_ftps

    @property
    def excludes(self):
        """List of paths to exclude from copying

        :rtype: list[str | unicode] | None
        """
        return self._excludes

    @property
    def encoding(self):
        """Encoding in which FTP server returns directory listings

        :rtype: str | unicode | None
        """
        return self._encoding

    @property
    def ignore_passive_mode_server_ip(self):
        """Whether to ignore IP address returned by FTP server when opening data channel in passive mode

        When is set to true, IP address returned by FTP server when opening data channel
        in passive mode is ignored. That option could be necessary because some misconfigured
        FTP servers return internal IPs, which could not be used to connect to the server.
        When the option is set, hostname specified for control connection to the FTP server
        will be used for data connections.

        :rtype: bool | None
        """
        return self._ignore_passive_mode_server_ip

    @property
    def list_empty_properties(self):
        """List of non-filled properties

        :rtype: list
        """
        list_empty = []
        if self._local_path is None:
            list_empty.append('local_path')
        if self._remote_path is None:
            list_empty.append('remote_path')
        if self._server is None:
            list_empty.append('server')
        if self._user is None:
            list_empty.append('user')
        if self._password is None:
            list_empty.append('password')
        return list_empty

    @property
    def ready_for_copy(self):
        """Returns 'True' if all properties are filled

        :rtype: bool
        """
        return len(self.list_empty_properties) == 0


class ConfigReader(object):
    """Configuration for the migration"""

    @staticmethod
    def read_from_args_and_file():
        """Build config from command line arguments or/and config file.
        Options obtained from arguments have priority over options obtained from config file.

        :rtype: ftp_migrator.main.Config
        """
        parser = argparse.ArgumentParser()
        parser.add_argument(
            '-l', '--local-path', dest='local_path', help='where to copy on local server'
        )
        parser.add_argument(
            '-r', '--remote-path', dest='remote_path', help='where from copy on remote server'
        )
        parser.add_argument(
            '-s', '--server', dest='server', help='IP address or hostname of the remote FTP server'
        )
        parser.add_argument(
            '-u', '--user', dest='user', help='FTP user login'
        )
        parser.add_argument(
            '-p', '--password', dest='password', help='FTP user password'
        )
        parser.add_argument(
            '-n', '--encoding', dest='encoding', help='encoding in which FTP server returns directory listings'
        )
        parser.add_argument(
            '-t', '--use-ftps', dest='use_ftps', action='store_true',
            help='whether to use FTPS (FTP +TLS) instead of plain FTP'
        )
        parser.add_argument(
            '-i', '--ignore-passive-mode-server-ip', dest='ignore_passive_mode_server_ip', action='store_true',
            help='whether to ignore IP address returned by FTP server when opening data channel in passive mode'
        )
        parser.add_argument(
            '-e', '--exclude', action='append', dest='exclude', help='define paths to exclude'
        )
        parser.add_argument(
            '-c', '--config-file', type=argparse.FileType('r'), dest='config_file',
            help='configuration file in json format'
        )
        args = parser.parse_args()
        config = Config(
            local_path=args.local_path,
            remote_path=args.remote_path,
            server=args.server,
            user=args.user,
            password=args.password,
            use_ftps=args.use_ftps,
            excludes=args.exclude,
            encoding=args.encoding,
            ignore_passive_mode_server_ip=args.ignore_passive_mode_server_ip
        )
        if args.config_file is not None:
            file_config = ConfigReader().read_from_file_object(args.config_file)
            config = ConfigReader().merge_configs(file_config, config)
        return config

    @staticmethod
    def read_from_file_object(config_file_object):
        """Build config from config file.

        :rtype: ftp_migrator.main.Config
        """
        data = json.load(config_file_object)
        local_path = data.get('local_path', None)
        remote_path = data.get('remote_path', None)
        return Config(
            local_path=local_path.encode('utf-8') if local_path is not None else None,
            remote_path=remote_path.encode('utf-8') if remote_path is not None else None,
            server=data.get('server', None),
            user=data.get('user', None),
            password=data.get('password', None),
            use_ftps=data.get('use_ftps') in (1, True, '1', 'true'),
            excludes=data.get('exclude'),
            encoding=data.get('encoding'),
            ignore_passive_mode_server_ip=data.get('ignore_passive_mode_server_ip') in (1, True, '1', 'true'),
        )

    @staticmethod
    def merge_configs(old_config, new_config):
        """Merge 2 configs with priority of new_config.

        :rtype: ftp_migrator.main.Config
        """
        return Config(
            local_path=new_config.local_path if new_config.local_path is not None else old_config.local_path,
            remote_path=new_config.remote_path if new_config.remote_path is not None else old_config.remote_path,
            server=new_config.server if new_config.server is not None else old_config.server,
            user=new_config.user if new_config.user is not None else old_config.user,
            password=new_config.password if new_config.password is not None else old_config.password,
            use_ftps=new_config.use_ftps or old_config.use_ftps,
            excludes=new_config.excludes if new_config.excludes is not None else old_config.excludes,
            encoding=new_config.encoding if new_config.encoding is not None else old_config.encoding,
            ignore_passive_mode_server_ip=(
                new_config.ignore_passive_mode_server_ip or old_config.ignore_passive_mode_server_ip
            ),
        )


def configure_logging():
    plesk_logger = logging.getLogger('plesk')
    plesk_logger.setLevel(logging.DEBUG)
    console_handler = logging.StreamHandler(stream=sys.stderr)
    console_handler.setLevel(logging.DEBUG)
    console_handler.setFormatter(logging.Formatter("[%(asctime)s] [%(levelname)s] %(message)s"))
    plesk_logger.addHandler(console_handler)


def main():
    configure_logging()

    config = ConfigReader().read_from_args_and_file()
    if not config.ready_for_copy:
        logger.error('Required: ' + ' '.join(config.list_empty_properties))
        return

    issues = []

    ftp = Ftp(
        config.server, config.user, config.password,
        config.use_ftps, config.encoding, config.ignore_passive_mode_server_ip
    )
    result = {
        'server_version': safe_string_repr(ftp.server_version)
    }

    try:
        issues = ftp.download_path(config.remote_path, config.local_path, config.excludes)
    except Exception as e:
        logger.debug(messages.LOG_EXCEPTION, exc_info=True)
        message = safe_format(
            messages.COMPLETELY_FAILED_TO_DOWNLOAD_DIRECTORY,
            source_path=config.remote_path, target_path=config.local_path
        )
        logger.error(message)
        issues.append(Issue(message, safe_string_repr(e)))
    finally:
        ftp.close()

    result['issues'] = [issue.as_dictionary() for issue in issues]

    print(json.dumps(result, indent=4))


if __name__ == '__main__':
    main()
