import fnmatch
import logging
import os
import stat

import pyftpdlib.authorizers
import pyftpdlib.handlers
import pyftpdlib.servers

from mini_buildd import changes, config, threads, util

LOG = logging.getLogger(__name__)


class FtpDHandlerMixin():
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._mbd_files_received = []

    def on_file_received(self, file):
        """Make any incoming file read-only as soon as it arrives; avoids overriding uploads of the same file"""
        os.chmod(file, stat.S_IRUSR | stat.S_IRGRP)
        self._mbd_files_received.append(file)
        LOG.debug("File received: %s", file)

    def on_incomplete_file_received(self, file):
        LOG.warning("Incomplete file received: %s", file)
        self._mbd_files_received.append(file)

    def on_disconnect(self):
        for file_path in (f for f in self._mbd_files_received if fnmatch.fnmatch(f, "*.changes")):
            try:
                changes.incoming2queue(file_path)
            except Exception as e:
                util.log_exception(LOG, f"Invalid incoming changes: {file_path}", e)

        # All received files should be queued by now, any left-over in received files is cruft
        for file_path in (f for f in self._mbd_files_received if os.path.exists(f)):
            LOG.warning("Removing cruft incoming: %s", file_path)
            os.remove(file_path)


class FtpDHandler(FtpDHandlerMixin, pyftpdlib.handlers.FTPHandler):
    pass


class FtpsDHandler(FtpDHandlerMixin, pyftpdlib.handlers.TLS_FTPHandler):
    pass


class FtpD(threads.Thread):
    HANDLER_OPTIONS_NAMES = ["passive_ports"]
    HANDLER_OPTIONS_USAGE = f"""\
ftpd handler options (see ``https://pyftpdlib.readthedocs.io/en/latest/api.html#control-connection``).

Format is ``name0=value0;name1=value1...``.

Supported options: ``{','.join(HANDLER_OPTIONS_NAMES)}``

Divergent string values:

``passive_ports=<min_port>-<max_port>``
"""

    @classmethod
    def parse_handler_options(cls, str_options):
        try:
            parsed = {o.split("=")[0]: o.split("=")[1] for o in (str_options.split(";") if str_options else [])}
        except BaseException as e:
            raise util.HTTPBadRequest(f"ftpd: Error parsing handler options syntax: {str_options}") from e

        try:
            result = {}
            for name, str_value in parsed.items():
                if name not in cls.HANDLER_OPTIONS_NAMES:
                    raise util.HTTPBadRequest(f"ftpd: Unhandled handler option: {name}")
                # Transfer values where needed
                result[name] = {
                    "passive_ports": lambda v: list(range(int(v.split("-")[0]), int(v.split("-")[1]) + 1)),
                }.get(name, lambda v: v)(str_value)
            return result
        except BaseException as e:
            raise util.HTTPBadRequest(f"ftpd: Error parsing handler options values: {e}") from e

    def __init__(self, endpoint):
        super().__init__()
        self.endpoint = endpoint
        self.ftpd = None

    def __str__(self):
        return f"{super().__str__()} ({self.endpoint.geturl()})"

    def bind(self, handler_options):
        if self.endpoint.is_ssl():
            handler = FtpsDHandler
            handler.certfile = self.endpoint.options.get("certKey")
            handler.keyfile = self.endpoint.options.get("privateKey")
            handler.tls_control_required = True
            handler.tls_data_required = True
        else:
            handler = FtpDHandler

        handler.authorizer = pyftpdlib.authorizers.DummyAuthorizer()
        handler.authorizer.add_anonymous(homedir=config.ROUTES["home"].path.join(), perm="")
        handler.authorizer.override_perm(username="anonymous", directory=config.ROUTES["incoming"].path.join(), perm="elrw")

        options = self.parse_handler_options(handler_options)
        for name, value in options.items():
            LOG.info("ftpd: Setting handler option: %s=%s", name, value)
            setattr(handler, name, value)

        handler.banner = f"mini-buildd {util.__version__} ftp server ready (pyftpdlib {pyftpdlib.__ver__})."

        try:
            self.ftpd = pyftpdlib.servers.FTPServer((self.endpoint.options.get("interface"), self.endpoint.options.get("port")), handler)
        except OSError as e:
            raise util.HTTPUnavailable(f"OS error: {e}")

    def mbd_run(self):
        self.ftpd.serve_forever(timeout=1.0)

    def shutdown(self):
        if self.ftpd is not None:
            self.ftpd.close_all()
