# coding=utf-8
"""
"""
from __future__ import absolute_import, print_function, division

import logging
import json
import time
from datetime import timedelta
from uuid import UUID, uuid1

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

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):
  try:
    UUID(handle)
  except ValueError:
    return False

  return True


class FileUploadsExtension(object):
  """
  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):
    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 = {}
    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(0o775)

    path.resolve()

  def _do_register_js_api(self, sender):
    app = sender
    js_api = app.js_api.setdefault('upload', {})
    js_api['newFileUrl'] = url_for('uploads.new_file')

  def user_dir(self, user):
    user_id = (str(user.id) if not isinstance(user, AnonymousUserMixin)
               else 'anonymous')
    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(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), ''):
        out.write(chunk)

    if metadata:
      meta_file = user_dir / '{}.metadata'.format(handle)
      with meta_file.open('wb') as out:
        json.dump(metadata, out, skipkeys=True)

    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 / '{}.metadata'.format(handle)
    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:
      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:
          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()


@shared_task
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()
