import os
import stat
import glob
import shutil
import fnmatch
import logging

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

import debian.deb822

import mini_buildd
import mini_buildd.config
import mini_buildd.misc
import mini_buildd.threads

LOG = logging.getLogger(__name__)


class Incoming():
    """Tool collection for some extra incoming directory handling."""

    @classmethod
    def is_changes(cls, file_name):
        return fnmatch.fnmatch(file_name, "*.changes")

    @classmethod
    def get_changes(cls):
        return glob.glob(mini_buildd.config.ROUTES["incoming"].path.join("*.changes"))

    @classmethod
    def remove_cruft_files(cls, files):
        """Remove all files from list of files not mentioned in a changes file."""
        valid_files = []
        for changes_file in files:
            if cls.is_changes(changes_file):
                LOG.debug(f"Checking: {changes_file}")
                try:
                    with mini_buildd.fopen(changes_file) as cf:
                        for fd in debian.deb822.Changes(cf).get("Files", []):
                            valid_files.append(fd["name"])
                            LOG.debug(f"Valid: {fd['name']}")

                        valid_files.append(os.path.basename(changes_file))
                except BaseException as e:
                    mini_buildd.log_exception(LOG, f"Invalid changes file: {changes_file}", e)

        for f in files:
            if os.path.basename(f) not in valid_files:
                # Be sure to never ever fail, just because cruft removal fails (instead log accordingly)
                try:
                    if os.path.isdir(f):
                        shutil.rmtree(f)
                    else:
                        os.remove(f)
                    LOG.warning(f"Cruft file (not in any changes file) removed: {f}")
                except BaseException as e:
                    mini_buildd.log_exception(LOG, f"Can't remove cruft from incoming: {f}", e, logging.CRITICAL)

    @classmethod
    def remove_cruft(cls):
        """Remove cruft files from incoming."""
        cls.remove_cruft_files([mini_buildd.config.ROUTES["incoming"].path.join(f) for f in os.listdir(mini_buildd.config.ROUTES["incoming"].path.join())])

    @classmethod
    def requeue_changes(cls, queue):
        """
        Re-queue all existing changes in incoming.

        We must feed the the user uploads first, so the daemon
        does not get any yet-unknown build results (hence the
        sorting).
        """
        for c in sorted(cls.get_changes(), key=lambda c: 1 if fnmatch.fnmatch(c, "*mini-buildd-build*") else 0):
            LOG.info(f"Incoming: Re-queuing: {c}")
            queue.put(c)


class FtpDHandlerMixin():
    mini_buildd_queue = None

    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.info(f"File received: {file}")

    def on_incomplete_file_received(self, file):
        LOG.warning(f"Incomplete file received: {file}")
        self._mbd_files_received.append(file)

    def on_disconnect(self):
        for file_name in (f for f in self._mbd_files_received if Incoming.is_changes(f)):
            LOG.info(f"Queuing incoming changes file: {file_name}")
            self.mini_buildd_queue.put(file_name)
        Incoming.remove_cruft_files(self._mbd_files_received)


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


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


class FtpD(mini_buildd.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 mini_buildd.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 mini_buildd.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 mini_buildd.HTTPBadRequest(f"ftpd: Error parsing handler options values: {e}") from e

    def __init__(self, endpoint, queue, handler_options):
        super().__init__(name=endpoint.geturl())
        self.endpoint = endpoint
        self.queue = queue
        self.ftpd = None
        self.handler_options = handler_options

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

    def run(self):
        mini_buildd.misc.clone_log("pyftpdlib")

        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=mini_buildd.config.ROUTES["home"].path.join(), perm="")
        handler.authorizer.override_perm(username="anonymous", directory=mini_buildd.config.ROUTES["incoming"].path.join(), perm="elrw")

        try:
            options = self.parse_handler_options(self.handler_options)
            for name, value in options.items():
                LOG.info(f"ftpd: Setting handler option: {name}={value}")
                setattr(handler, name, value)
        except BaseException as e:
            # In case there should be an error, mini-buildd won't start. Rather start anyway, else error can't be fixed at all.
            mini_buildd.log_exception(LOG, "ftpd: Error setting handler options (ignoring, please fix anyway!)", e)

        handler.banner = f"mini-buildd {mini_buildd.__version__} ftp server ready (pyftpdlib {pyftpdlib.__ver__})."
        handler.mini_buildd_queue = self.queue

        self.ftpd = pyftpdlib.servers.FTPServer((self.endpoint.options.get("interface"), self.endpoint.options.get("port")), handler)

        Incoming.remove_cruft()
        Incoming.requeue_changes(self.queue)

        self.ftpd.serve_forever(timeout=1.0)
        LOG.debug(f"{self}: FINISHED")
