""""""
import json
import logging
import time
from datetime import timedelta
from typing import Any, Dict
from uuid import UUID, uuid1

from celery import shared_task
from flask import current_app
from flask_login import AnonymousUserMixin

from abilian.app import Application
from abilian.core import signals
from abilian.web import url_for

from .views import bp as blueprint

logger = logging.getLogger(__name__)

CHUNK_SIZE = 64 * 1024

DEFAULT_CONFIG = {
    "USER_QUOTA": 100 * 1024 ** 2,  # max 100 Mb for all current files
    "USER_MAX_FILES": 1000,  # max number of files per user
    "DELETE_STALLED_AFTER": 60 * 60 * 24,  # delete files remaining after 1 day
}

CLEANUP_SCHEDULE_ID = __name__ + ".periodic_clean_upload_directory"
DEFAULT_CLEANUP_SCHEDULE = {"task": CLEANUP_SCHEDULE_ID, "schedule": timedelta(hours=1)}


def is_valid_handle(handle: str) -> bool:
    try:
        UUID(handle)
    except ValueError:
        return False

    return True


class FileUploadsExtension:
    """API for Out-Of-Band file uploads.

    Allow to manage files in forms: file is uploaded to an upload url, a handle is
    returned will be used in the form to refer to this uploaded filed.

    If the form fails to validate the uploaded file is not lost.

    A periodic task cleans the temporary repository.
    """

    def __init__(self, app: Application) -> None:
        app.extensions["uploads"] = self
        app.add_template_global(self, "uploads")
        app.register_blueprint(blueprint)
        signals.register_js_api.connect(self._do_register_js_api)

        self.config: Dict[str, Any] = {}
        self.config.update(DEFAULT_CONFIG)
        self.config.update(app.config.get("FILE_UPLOADS", {}))
        app.config["FILE_UPLOADS"] = self.config

        # celery schedule
        CELERYBEAT_SCHEDULE = app.config.setdefault("CELERYBEAT_SCHEDULE", {})
        if CLEANUP_SCHEDULE_ID not in CELERYBEAT_SCHEDULE:
            CELERYBEAT_SCHEDULE[CLEANUP_SCHEDULE_ID] = DEFAULT_CLEANUP_SCHEDULE

        path = self.UPLOAD_DIR = app.data_dir / "uploads"
        if not path.exists():
            path.mkdir(mode=0o775, parents=True)

        path.resolve()

    def _do_register_js_api(self, sender: Application) -> None:
        app = sender
        # pyre-fixme[6]: Expected `str` for 2nd param but got `Dict[_KT, _VT]`.
        js_api = app.js_api.setdefault("upload", {})
        # pyre-fixme[16]: `str` has no attribute `__setitem__`.
        js_api["newFileUrl"] = url_for("uploads.new_file")

    def user_dir(self, user):
        if isinstance(user, AnonymousUserMixin):
            user_id = "anonymous"
        else:
            user_id = str(user.id)
        return self.UPLOAD_DIR / user_id

    def add_file(self, user, file_obj, **metadata):
        """Add a new file.

        :returns: file handle
        """
        user_dir = self.user_dir(user)
        if not user_dir.exists():
            user_dir.mkdir(mode=0o775)

        handle = str(uuid1())
        file_path = user_dir / handle

        with file_path.open("wb") as out:
            for chunk in iter(lambda: file_obj.read(CHUNK_SIZE), b""):
                out.write(chunk)

        if metadata:
            meta_file = user_dir / f"{handle}.metadata"
            with meta_file.open("wb") as out:
                metadata_json = json.dumps(metadata, skipkeys=True).encode("ascii")
                out.write(metadata_json)

        return handle

    def get_file(self, user, handle):
        """Retrieve a file for a user.

        :returns: a :class:`pathlib.Path` instance to this file,
            or None if no file can be found for this handle.
        """
        user_dir = self.user_dir(user)
        if not user_dir.exists():
            return None

        if not is_valid_handle(handle):
            return None

        file_path = user_dir / handle

        if not file_path.exists() and not file_path.is_file():
            return None

        return file_path

    def get_metadata_file(self, user, handle):
        content = self.get_file(user, handle)
        if content is None:
            return None

        metafile = content.parent / f"{handle}.metadata"
        if not metafile.exists():
            return None

        return metafile

    def get_metadata(self, user, handle):
        metafile = self.get_metadata_file(user, handle)
        if metafile is None:
            return {}

        try:
            with metafile.open("rb") as in_:
                meta = json.load(in_)
        except Exception:
            meta = {}

        return meta

    def remove_file(self, user, handle):
        paths = (self.get_file(user, handle), self.get_metadata_file(user, handle))

        for file_path in paths:
            if file_path is not None:
                try:
                    file_path.unlink()
                except Exception:
                    logger.exception("Error during remove file")

    def clear_stalled_files(self):
        """Scan upload directory and delete stalled files.

        Stalled files are files uploaded more than
        `DELETE_STALLED_AFTER` seconds ago.
        """
        # FIXME: put lock in directory?
        CLEAR_AFTER = self.config["DELETE_STALLED_AFTER"]
        minimum_age = time.time() - CLEAR_AFTER

        for user_dir in self.UPLOAD_DIR.iterdir():
            if not user_dir.is_dir():
                logger.error("Found non-directory in upload dir: %r", bytes(user_dir))
                continue

            for content in user_dir.iterdir():
                if not content.is_file():
                    logger.error(
                        "Found non-file in user upload dir: %r", bytes(content)
                    )
                    continue

                if content.stat().st_ctime < minimum_age:
                    content.unlink()


# Task scheduled to run every hour: make it expire after 50min.
@shared_task(expires=3000)
def periodic_clean_upload_directory():
    """This task should be run periodically.

    Default config sets up schedule using
    :data:`DEFAULT_CLEANUP_SCHEDULE`. `CELERYBEAT_SCHEDULE` key is
    :data:`CLEANUP_SCHEDULE_ID`.
    """
    uploads = current_app.extensions["uploads"]
    uploads.clear_stalled_files()
