# -*- coding: utf-8 -*-
import base64
import hashlib
import json
import logging
import os
import shutil
import subprocess
import sys
import tempfile
import traceback
import urlparse
import uuid
import zipfile

from django import forms
from django.conf import settings
from django.core.files.storage import default_storage as storage

import requests
from appvalidator import validate_app, validate_packaged_app
from celery import task
from django_statsd.clients import statsd
from PIL import Image
from tower import ugettext as _

import mkt
from lib.post_request_task.task import task as post_request_task
from mkt.constants import APP_PREVIEW_SIZES
from mkt.constants.regions import REGIONS_CHOICES_ID_DICT
from mkt.files.helpers import copyfileobj
from mkt.files.models import File, FileUpload, FileValidation
from mkt.files.utils import SafeUnzip
from mkt.site.decorators import set_modified_on, use_master
from mkt.site.helpers import absolutify
from mkt.site.mail import send_mail_jinja
from mkt.site.storage_utils import private_storage
from mkt.site.utils import (remove_icons, remove_promo_imgs, resize_image,
                            strip_bom)
from mkt.webapps.models import AddonExcludedRegion, Preview, Webapp
from mkt.webapps.utils import iarc_get_app_info


log = logging.getLogger('z.mkt.developers.task')


CT_URL = (
    'https://developer.mozilla.org/docs/Web/Apps/Manifest#Serving_manifests'
)

REQUESTS_HEADERS = {
    'User-Agent': 'Mozilla/5.0 (Mobile; rv:18.0) Gecko/18.0 Firefox/18.0'
}


@post_request_task
@use_master
def validator(upload_id, **kw):
    if not settings.VALIDATE_ADDONS:
        return None
    log.info(u'[FileUpload:%s] Validating app.' % upload_id)
    try:
        upload = FileUpload.objects.get(pk=upload_id)
    except FileUpload.DoesNotExist:
        log.info(u'[FileUpload:%s] Does not exist.' % upload_id)
        return

    try:
        validation_result = run_validator(upload.path, url=kw.get('url'))
        if upload.validation:
            # If there's any preliminary validation result, merge it with the
            # actual validation result.
            dec_prelim_result = json.loads(upload.validation)
            if 'prelim' in dec_prelim_result:
                dec_validation_result = json.loads(validation_result)
                # Merge the messages.
                dec_validation_result['messages'] += (
                    dec_prelim_result['messages'])
                # Merge the success value.
                if dec_validation_result['success']:
                    dec_validation_result['success'] = (
                        dec_prelim_result['success'])
                # Merge the error count (we only raise errors, not warnings).
                dec_validation_result['errors'] += dec_prelim_result['errors']

                # Put the validation result back into JSON.
                validation_result = json.dumps(dec_validation_result)

        upload.validation = validation_result
        upload.save()  # We want to hit the custom save().
    except Exception:
        # Store the error with the FileUpload job, then raise
        # it for normal logging.
        tb = traceback.format_exception(*sys.exc_info())
        upload.update(task_error=''.join(tb))
        # Don't raise if we're being eager, setting the error is enough.
        if not settings.CELERY_ALWAYS_EAGER:
            raise


@task
@use_master
def file_validator(file_id, **kw):
    if not settings.VALIDATE_ADDONS:
        return None
    log.info(u'[File:%s] Validating file.' % file_id)
    try:
        file = File.objects.get(pk=file_id)
    except File.DoesNotExist:
        log.info(u'[File:%s] Does not exist.' % file_id)
        return
    # Unlike upload validation, let the validator raise an exception if there
    # is one.
    result = run_validator(file.file_path, url=file.version.addon.manifest_url)
    return FileValidation.from_json(file, result)


def run_validator(file_path, url=None):
    """A pre-configured wrapper around the app validator."""

    temp_path = None
    # Make a copy of the file since we can't assume the
    # uploaded file is on the local filesystem.
    temp_path = tempfile.mktemp()
    with open(temp_path, 'wb') as local_f:
        with private_storage.open(file_path) as remote_f:
            copyfileobj(remote_f, local_f)

    with statsd.timer('mkt.developers.validator'):
        is_packaged = zipfile.is_zipfile(temp_path)
        if is_packaged:
            log.info(u'Running `validate_packaged_app` for path: %s'
                     % (file_path))
            with statsd.timer('mkt.developers.validate_packaged_app'):
                return validate_packaged_app(
                    temp_path,
                    market_urls=settings.VALIDATOR_IAF_URLS,
                    timeout=settings.VALIDATOR_TIMEOUT,
                    spidermonkey=settings.SPIDERMONKEY)
        else:
            log.info(u'Running `validate_app` for path: %s' % (file_path))
            with statsd.timer('mkt.developers.validate_app'):
                return validate_app(open(temp_path).read(),
                                    market_urls=settings.VALIDATOR_IAF_URLS,
                                    url=url)

    # Clean up copied files.
    os.unlink(temp_path)


def _hash_file(fd):
    return hashlib.md5(fd.read()).hexdigest()[:8]


@post_request_task
@set_modified_on
def resize_icon(src, dst, sizes, locally=False, **kw):
    """Resizes addon/websites icons."""
    log.info('[1@None] Resizing icon: %s' % dst)

    open_ = open if locally else storage.open
    delete = os.unlink if locally else storage.delete

    try:
        for s in sizes:
            size_dst = '%s-%s.png' % (dst, s)
            resize_image(src, size_dst, (s, s),
                         remove_src=False, locally=locally)
            pngcrush_image.delay(size_dst, **kw)

        with open_(src) as fd:
            icon_hash = _hash_file(fd)
        delete(src)

        log.info('Icon resizing completed for: %s' % dst)
        return {'icon_hash': icon_hash}
    except Exception, e:
        log.error("Error resizing icon: %s; %s" % (e, dst))


@post_request_task
@set_modified_on
def resize_promo_imgs(src, dst, sizes, locally=False, **kw):
    """Resizes webapp/website promo imgs."""
    log.info('[1@None] Resizing promo imgs: %s' % dst)
    try:
        for s in sizes:
            size_dst = '%s-%s.png' % (dst, s)
            # Crop only to the width, keeping the aspect ratio.
            resize_image(src, size_dst, (s, 0),
                         remove_src=False, locally=locally)
            pngcrush_image.delay(size_dst, **kw)

        if locally:
            with open(src) as fd:
                promo_img_hash = _hash_file(fd)
            os.remove(src)
        else:
            with storage.open(src) as fd:
                promo_img_hash = _hash_file(fd)
            storage.delete(src)

        log.info('Promo img hash resizing completed for: %s' % dst)
        return {'promo_img_hash': promo_img_hash}
    except Exception, e:
        log.error("Error resizing promo img hash: %s; %s" % (e, dst))


@task
@set_modified_on
def pngcrush_image(src, hash_field='image_hash', **kw):
    """
    Optimizes a PNG image by running it through Pngcrush. Returns hash.

    src -- filesystem image path
    hash_field -- field name to save the new hash on instance if passing
                  instance through set_modified_on
    """
    log.info('[1@None] Optimizing image: %s' % src)
    try:
        # pngcrush -ow has some issues, use a temporary file and do the final
        # renaming ourselves.
        suffix = '.opti.png'
        tmp_path = '%s%s' % (os.path.splitext(src)[0], suffix)
        cmd = [settings.PNGCRUSH_BIN, '-q', '-rem', 'alla', '-brute',
               '-reduce', '-e', suffix, src]
        sp = subprocess.Popen(cmd, stdin=subprocess.PIPE,
                              stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        stdout, stderr = sp.communicate()

        if sp.returncode != 0:
            log.error('Error optimizing image: %s; %s' % (src, stderr.strip()))
            pngcrush_image.retry(args=[src], kwargs=kw, max_retries=3)
            return False

        shutil.move(tmp_path, src)
        log.info('Image optimization completed for: %s' % src)

        # Return hash for set_modified_on.
        with open(src) as fd:
            image_hash = _hash_file(fd)

        return {
            hash_field: image_hash
        }

    except Exception, e:
        log.error('Error optimizing image: %s; %s' % (src, e))


@post_request_task
@set_modified_on
def resize_preview(src, pk, **kw):
    """Resizes preview images and stores the sizes on the preview."""
    instance = Preview.objects.get(pk=pk)
    thumb_dst, full_dst = instance.thumbnail_path, instance.image_path
    sizes = instance.sizes or {}
    log.info('[1@None] Resizing preview and storing size: %s' % thumb_dst)
    try:
        thumbnail_size = APP_PREVIEW_SIZES[0][:2]
        image_size = APP_PREVIEW_SIZES[1][:2]
        with storage.open(src, 'rb') as fp:
            size = Image.open(fp).size
        if size[0] > size[1]:
            # If the image is wider than tall, then reverse the wanted size
            # to keep the original aspect ratio while still resizing to
            # the correct dimensions.
            thumbnail_size = thumbnail_size[::-1]
            image_size = image_size[::-1]

        if kw.get('generate_thumbnail', True):
            sizes['thumbnail'] = resize_image(src, thumb_dst,
                                              thumbnail_size,
                                              remove_src=False)
        if kw.get('generate_image', True):
            sizes['image'] = resize_image(src, full_dst,
                                          image_size,
                                          remove_src=False)
        instance.sizes = sizes
        instance.save()
        log.info('Preview resized to: %s' % thumb_dst)

        # Remove src file now that it has been processed.
        try:
            os.remove(src)
        except OSError:
            pass

        return True

    except Exception, e:
        log.error("Error saving preview: %s; %s" % (e, thumb_dst))


def _fetch_content(url):
    with statsd.timer('developers.tasks.fetch_content'):
        try:
            res = requests.get(url, timeout=30, stream=True,
                               headers=REQUESTS_HEADERS)

            if not 200 <= res.status_code < 300:
                statsd.incr('developers.tasks.fetch_content.error')
                raise Exception('An invalid HTTP status code was returned.')

            if not res.headers.keys():
                statsd.incr('developers.tasks.fetch_content.error')
                raise Exception('The HTTP server did not return headers.')

            statsd.incr('developers.tasks.fetch_content.success')
            return res
        except requests.RequestException as e:
            statsd.incr('developers.tasks.fetch_content.error')
            log.error('fetch_content connection error: %s' % e)
            raise Exception('The file could not be retrieved.')


class ResponseTooLargeException(Exception):
    pass


def get_content_and_check_size(response, max_size):
    # Read one extra byte. Reject if it's too big so we don't have issues
    # downloading huge files.
    content = response.iter_content(chunk_size=max_size + 1).next()
    if len(content) > max_size:
        raise ResponseTooLargeException('Too much data.')
    return content


def save_icon(obj, icon_content):
    """
    Saves the icon for `obj` to its final destination. `obj` can be an app or a
    website.
    """
    tmp_dst = os.path.join(settings.TMP_PATH, 'icon', uuid.uuid4().hex)
    with storage.open(tmp_dst, 'wb') as fd:
        fd.write(icon_content)

    dirname = obj.get_icon_dir()
    destination = os.path.join(dirname, '%s' % obj.pk)
    remove_icons(destination)
    icon_hash = resize_icon(tmp_dst, destination, mkt.CONTENT_ICON_SIZES,
                            set_modified_on=[obj])

    # Need to set icon type so .get_icon_url() works normally
    # submit step 4 does it through AppFormMedia, but we want to beat them to
    # the punch. resize_icon outputs pngs so we know it's 'image/png'.
    obj.icon_hash = icon_hash['icon_hash']  # In case, we're running not async.
    obj.icon_type = 'image/png'
    obj.save()


def save_promo_imgs(obj, img_content):
    """
    Saves the promo image for `obj` to its final destination.
    `obj` can be an app or a website.
    """
    tmp_dst = os.path.join(settings.TMP_PATH, 'promo_imgs', uuid.uuid4().hex)
    with storage.open(tmp_dst, 'wb') as fd:
        fd.write(img_content)

    dirname = obj.get_promo_img_dir()
    destination = os.path.join(dirname, '%s' % obj.pk)
    remove_promo_imgs(destination)
    resize_promo_imgs(
        tmp_dst, destination, mkt.PROMO_IMG_SIZES,
        set_modified_on=[obj])


@post_request_task
@use_master