# -*- coding: utf-8 -*-
import json
import mimetypes
import os
from datetime import datetime
from zipfile import ZipFile

from django import forms
from django.conf import settings
from django.core.validators import URLValidator
from django.forms import widgets
from django.forms.extras.widgets import SelectDateWidget
from django.forms.models import modelformset_factory
from django.template.defaultfilters import filesizeformat
from django.utils import six
from django.utils.functional import lazy
from django.utils.safestring import mark_safe
from django.utils.translation import trans_real as translation

import commonware
import happyforms
import waffle
from jinja2 import escape as jinja2_escape
from jinja2.filters import do_dictsort
from mpconstants import regions as mpconstants_regions
from quieter_formset.formset import BaseModelFormSet
from tower import ugettext as _, ugettext_lazy as _lazy, ungettext as ngettext

import lib.iarc
import mkt
from lib.video import tasks as vtasks
from mkt import get_user
from mkt.access import acl
from mkt.api.models import Access
from mkt.constants import (CATEGORY_CHOICES, MAX_PACKAGED_APP_SIZE,
                           ratingsbodies)
from mkt.developers.utils import prioritize_app
from mkt.files.models import FileUpload
from mkt.files.utils import WebAppParser
from mkt.regions import REGIONS_CHOICES_SORTED_BY_NAME
from mkt.regions.utils import parse_region
from mkt.reviewers.models import RereviewQueue
from mkt.site.fields import SeparatedValuesField
from mkt.site.forms import AddonChoiceField
from mkt.site.utils import remove_icons, slug_validator, slugify
from mkt.tags.models import Tag
from mkt.tags.utils import can_edit_restricted_tags, clean_tags
from mkt.translations.fields import TransField
from mkt.translations.forms import TranslationFormMixin
from mkt.translations.models import Translation
from mkt.translations.widgets import TranslationTextarea, TransTextarea
from mkt.versions.models import Version
from mkt.webapps.models import (AddonUser, BlockedSlug, IARCInfo, Preview,
                                Webapp)
from mkt.webapps.tasks import (index_webapps, set_storefront_data,
                               update_manifests)

from . import tasks


log = commonware.log.getLogger('mkt.developers')


def region_error(region):
    return forms.ValidationError(_('You cannot select {region}.').format(
        region=unicode(parse_region(region).name)
    ))


def toggle_app_for_special_regions(request, app, enabled_regions=None):
    """Toggle for special regions (e.g., China)."""
    if not waffle.flag_is_active(request, 'special-regions'):
        return

    for region in mkt.regions.SPECIAL_REGIONS:
        status = app.geodata.get_status(region)

        if enabled_regions is not None:
            if region.id in enabled_regions:
                # If it's not already enabled, mark as pending.
                if status != mkt.STATUS_PUBLIC:
                    # Developer requested for it to be in China.
                    status = mkt.STATUS_PENDING
                    value, changed = app.geodata.set_status(region, status)
                    if changed:
                        log.info(u'[Webapp:%s] App marked as pending '
                                 u'special region (%s).' % (app, region.slug))
                        value, changed = app.geodata.set_nominated_date(
                            region, save=True)
                        log.info(u'[Webapp:%s] Setting nomination date to '
                                 u'now for region (%s).' % (app, region.slug))
            else:
                # Developer cancelled request for approval.
                status = mkt.STATUS_NULL
                value, changed = app.geodata.set_status(
                    region, status, save=True)
                if changed:
                    log.info(u'[Webapp:%s] App marked as null special '
                             u'region (%s).' % (app, region.slug))

        if status == mkt.STATUS_PUBLIC:
            # Reviewer approved for it to be in China.
            aer = app.addonexcludedregion.filter(region=region.id)
            if aer.exists():
                aer.delete()
                log.info(u'[Webapp:%s] App included in new special '
                         u'region (%s).' % (app, region.slug))
        else:
            # Developer requested for it to be in China.
            aer, created = app.addonexcludedregion.get_or_create(
                region=region.id)
            if created:
                log.info(u'[Webapp:%s] App excluded from new special '
                         u'region (%s).' % (app, region.slug))


class AuthorForm(happyforms.ModelForm):

    def clean_user(self):
        user = self.cleaned_data['user']
        if not user.read_dev_agreement:
            raise forms.ValidationError(
                _('All team members must have read and agreed to the '
                  'developer agreement.'))

        return user

    class Meta:
        model = AddonUser
        exclude = ('addon',)


class BaseModelFormSet(BaseModelFormSet):
    """
    Override the parent's is_valid to prevent deleting all forms.
    """

    def is_valid(self):
        # clean() won't get called in is_valid() if all the rows are getting
        # deleted. We can't allow deleting everything.
        rv = super(BaseModelFormSet, self).is_valid()
        return rv and not any(self.errors) and not bool(self.non_form_errors())


class BaseAuthorFormSet(BaseModelFormSet):

    def clean(self):
        if any(self.errors):
            return
        # cleaned_data could be None if it's the empty extra form.
        data = filter(None, [f.cleaned_data for f in self.forms
                             if not f.cleaned_data.get('DELETE', False)])
        if not any(d['role'] == mkt.AUTHOR_ROLE_OWNER for d in data):
            raise forms.ValidationError(_('Must have at least one owner.'))
        if not any(d['listed'] for d in data):
            raise forms.ValidationError(
                _('At least one team member must be listed.'))
        users = [d['user'] for d in data]
        if sorted(users) != sorted(set(users)):
            raise forms.ValidationError(
                _('A team member can only be listed once.'))


AuthorFormSet = modelformset_factory(AddonUser, formset=BaseAuthorFormSet,
                                     form=AuthorForm, can_delete=True, extra=0)


class DeleteForm(happyforms.Form):
    reason = forms.CharField(required=False)

    def __init__(self, request):
        super(DeleteForm, self).__init__(request.POST)


def trap_duplicate(request, manifest_url):
    # See if this user has any other apps with the same manifest.
    owned = (request.user.addonuser_set
             .filter(addon__manifest_url=manifest_url))
    if not owned:
        return
    try:
        app = owned[0].addon
    except Webapp.DoesNotExist:
        return
    error_url = app.get_dev_url()
    msg = None
    if app.status == mkt.STATUS_PUBLIC:
        msg = _(u'Oops, looks like you already submitted that manifest '
                'for %s, which is currently public. '
                '<a href="%s">Edit app</a>')
    elif app.status == mkt.STATUS_PENDING:
        msg = _(u'Oops, looks like you already submitted that manifest '
                'for %s, which is currently pending. '
                '<a href="%s">Edit app</a>')
    elif app.status == mkt.STATUS_NULL:
        msg = _(u'Oops, looks like you already submitted that manifest '
                'for %s, which is currently incomplete. '
                '<a href="%s">Resume app</a>')
    elif app.status == mkt.STATUS_REJECTED:
        msg = _(u'Oops, looks like you already submitted that manifest '
                'for %s, which is currently rejected. '
                '<a href="%s">Edit app</a>')
    elif app.status == mkt.STATUS_DISABLED:
        msg = _(u'Oops, looks like you already submitted that manifest '
                'for %s, which is currently banned on Marketplace. '
                '<a href="%s">Edit app</a>')
    elif app.disabled_by_user:
        msg = _(u'Oops, looks like you already submitted that manifest '
                'for %s, which is currently disabled. '
                '<a href="%s">Edit app</a>')
    if msg:
        return msg % (jinja2_escape(app.name), error_url)


def verify_app_domain(manifest_url, exclude=None, packaged=False):
    if packaged or waffle.switch_is_active('webapps-unique-by-domain'):
        domain = Webapp.domain_from_url(manifest_url)
        qs = Webapp.objects.filter(app_domain=domain)
        if exclude:
            qs = qs.exclude(pk=exclude.pk)
        if qs.exists():
            raise forms.ValidationError(
                _('An app already exists on this domain; '
                  'only one app per domain is allowed.'))


class PreviewForm(happyforms.ModelForm):
    file_upload = forms.FileField(required=False)
    upload_hash = forms.CharField(required=False)
    # This lets us POST the data URIs of the unsaved previews so we can still
    # show them if there were form errors.
    unsaved_image_data = forms.CharField(required=False,
                                         widget=forms.HiddenInput)
    unsaved_image_type = forms.CharField(required=False,
                                         widget=forms.HiddenInput)

    def save(self, addon, commit=True):
        if self.cleaned_data:
            self.instance.addon = addon
            if self.cleaned_data.get('DELETE'):
                # Existing preview.
                if self.instance.id:
                    self.instance.delete()
                # User has no desire to save this preview.
                return

            super(PreviewForm, self).save(commit=commit)
            if self.cleaned_data['upload_hash']:
                upload_hash = self.cleaned_data['upload_hash']
                upload_path = os.path.join(settings.TMP_PATH, 'preview',
                                           upload_hash)
                filetype = (os.path.splitext(upload_hash)[1][1:]
                                   .replace('-', '/'))
                if filetype in mkt.VIDEO_TYPES:
                    self.instance.update(filetype=filetype)
                    vtasks.resize_video.delay(upload_path, self.instance.pk,
                                              user_pk=mkt.get_user().pk,
                                              set_modified_on=[self.instance])
                else:
                    self.instance.update(filetype='image/png')
                    tasks.resize_preview.delay(upload_path, self.instance.pk,
                                               set_modified_on=[self.instance])

    class Meta:
        model = Preview
        fields = ('file_upload', 'upload_hash', 'id', 'position')


class JSONField(forms.Field):
    def to_python(self, value):
        if value == '':
            return None

        try:
            if isinstance(value, basestring):
                return json.loads(value)
        except ValueError:
            pass
        return value


class JSONMultipleChoiceField(forms.MultipleChoiceField, JSONField):
    widget = forms.CheckboxSelectMultiple


class AdminSettingsForm(PreviewForm):
    DELETE = forms.BooleanField(required=False)
    mozilla_contact = SeparatedValuesField(forms.EmailField, separator=',',
                                           required=False)
    vip_app = forms.BooleanField(required=False)
    priority_review = forms.BooleanField(required=False)
    banner_regions = JSONMultipleChoiceField(
        required=False, choices=mkt.regions.REGIONS_CHOICES_NAME)
    banner_message = TransField(required=False)

    class Meta:
        model = Preview
        fields = ('file_upload', 'upload_hash', 'position')

    def __init__(self, *args, **kw):
        # Note that this form is not inheriting from AddonFormBase, so we have
        # to get rid of 'version' ourselves instead of letting the parent class
        # do it.
        kw.pop('version', None)

        # Get the object for the app's promo `Preview` and pass it to the form.
        if kw.get('instance'):
            addon = kw.pop('instance')
            self.instance = addon
            self.promo = addon.get_promo()

        self.request = kw.pop('request', None)

        # Note: After calling `super`, `self.instance` becomes the `Preview`
        # object.
        super(AdminSettingsForm, self).__init__(*args, **kw)

        self.initial['vip_app'] = addon.vip_app
        self.initial['priority_review'] = addon.priority_review

        if self.instance:
            self.initial['mozilla_contact'] = addon.mozilla_contact

        self.initial['banner_regions'] = addon.geodata.banner_regions or []
        self.initial['banner_message'] = addon.geodata.banner_message_id

    @property
    def regions_by_id(self):
        return mkt.regions.REGIONS_CHOICES_ID_DICT

    def clean_position(self):
        return -1

    def clean_banner_regions(self):
        try:
            regions = map(int, self.cleaned_data.get('banner_regions'))
        except (TypeError, ValueError):
            # input data is not a list or data contains non-integers.
            raise forms.ValidationError(_('Invalid region(s) selected.'))

        return list(regions)

    def clean_mozilla_contact(self):
        contact = self.cleaned_data.get('mozilla_contact')
        if self.cleaned_data.get('mozilla_contact') is None:
            return u''
        return contact

    def save(self, addon, commit=True):
        if (self.cleaned_data.get('DELETE') and
                'upload_hash' not in self.changed_data and self.promo.id):
            self.promo.delete()
        elif self.promo and 'upload_hash' in self.changed_data:
            self.promo.delete()
        elif self.cleaned_data.get('upload_hash'):
            super(AdminSettingsForm, self).save(addon, True)

        updates = {
            'vip_app': self.cleaned_data.get('vip_app'),
        }
        contact = self.cleaned_data.get('mozilla_contact')
        if contact is not None:
            updates['mozilla_contact'] = contact
        if (self.cleaned_data.get('priority_review') and
                not addon.priority_review):
            # addon.priority_review gets updated within prioritize_app().
            prioritize_app(addon, self.request.user)
        else:
            updates['priority_review'] = self.cleaned_data.get(
                'priority_review')
        addon.update(**updates)

        geodata = addon.geodata
        geodata.banner_regions = self.cleaned_data.get('banner_regions')
        geodata.banner_message = self.cleaned_data.get('banner_message')
        geodata.save()

        uses_flash = self.cleaned_data.get('flash')
        af = addon.get_latest_file()
        if af is not None:
            af.update(uses_flash=bool(uses_flash))

        index_webapps.delay([addon.id])

        return addon


class BasePreviewFormSet(BaseModelFormSet):

    def clean(self):
        if any(self.errors):
            return
        at_least_one = False
        for form in self.forms:
            if (not form.cleaned_data.get('DELETE') and
                    form.cleaned_data.get('upload_hash') is not None):
                at_least_one = True
        if not at_least_one:
            raise forms.ValidationError(
                _('You must upload at least one screenshot or video.'))


PreviewFormSet = modelformset_factory(Preview, formset=BasePreviewFormSet,
                                      form=PreviewForm, can_delete=True,
                                      extra=1)


class NewManifestForm(happyforms.Form):
    manifest = forms.URLField()

    def __init__(self, *args, **kwargs):
        self.is_standalone = kwargs.pop('is_standalone', False)
        super(NewManifestForm, self).__init__(*args, **kwargs)

    def clean_manifest(self):
        manifest = self.cleaned_data['manifest']
        # Skip checking the domain for the standalone validator.
        if not self.is_standalone:
            verify_app_domain(manifest)
        return manifest


class NewPackagedAppForm(happyforms.Form):
    upload = forms.FileField()

    def __init__(self, *args, **kwargs):
        self.max_size = kwargs.pop('max_size', MAX_PACKAGED_APP_SIZE)
        self.user = kwargs.pop('user', get_user())
        self.addon = kwargs.pop('addon', None)
        self.file_upload = None
        super(NewPackagedAppForm, self).__init__(*args, **kwargs)

    def clean_upload(self):
        upload = self.cleaned_data['upload']
        errors = []

        if upload.size > self.max_size:
            errors.append({
                'type': 'error',
                'message': _('Packaged app too large for submission. Packages '
                             'must be smaller than %s.' % filesizeformat(
                                 self.max_size)),
                'tier': 1,
            })
            # Immediately raise an error, do not process the rest of the view,
            # which would read the file.
            raise self.persist_errors(errors, upload)

        manifest = None
        try:
            # Be careful to keep this as in-memory zip reading.
            manifest = ZipFile(upload, 'r').read('manifest.webapp')
        except Exception as e:
            errors.append({
                'type': 'error',
                'message': _('Error extracting manifest from zip file.'),
                'tier': 1,
            })

        origin = None
        if manifest:
            try:
                origin = WebAppParser.decode_manifest(manifest).get('origin')
            except forms.ValidationError as e:
                errors.append({
                    'type': 'error',
                    'message': ''.join(e.messages),
                    'tier': 1,
                })

        if origin:
            try:
                verify_app_domain(origin, packaged=True, exclude=self.addon)
            except forms.ValidationError, e:
                errors.append({
                    'type': 'error',
                    'message': ''.join(e.messages),
                    'tier': 1,
                })

        if errors:
            raise self.persist_errors(errors, upload)

        # Everything passed validation.
        self.file_upload = FileUpload.from_post(
            upload, upload.name, upload.size, user=self.user)

    def persist_errors(self, errors, upload):
        """
        Persist the error with this into FileUpload (but do not persist
        the file contents, which are too large) and return a ValidationError.
        """
        validation = {
            'errors': len(errors),
            'success': False,
            'messages': errors,
        }

        self.file_upload = FileUpload.objects.create(
            user=self.user, name=getattr(upload, 'name', ''),
            validation=json.dumps(validation))

        # Return a ValidationError to be raised by the view.
        return forms.ValidationError(' '.join(e['message'] for e in errors))


class AddonFormBase(TranslationFormMixin, happyforms.ModelForm):

    def __init__(self, *args, **kw):
        self.request = kw.pop('request')