import collections
import glob
import hashlib
import json
import logging
import os
import re
import shutil
import stat
import StringIO
import tempfile
import zipfile

from cStringIO import StringIO as cStringIO
from datetime import datetime
from itertools import groupby
from xml.dom import minidom
from zipfile import BadZipfile, ZipFile

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

import rdflib
from tower import ugettext as _

import amo
from amo.utils import rm_local_tmp_dir
from applications.models import AppVersion
from versions.compare import version_int as vint


log = logging.getLogger('z.files.utils')


class ParseError(forms.ValidationError):
    pass


VERSION_RE = re.compile('^[-+*.\w]{,32}$')
SIGNED_RE = re.compile('^META\-INF/(\w+)\.(rsa|sf)$')
# The default update URL.
default = (
    'https://versioncheck.addons.mozilla.org/update/VersionCheck.php?'
    'reqVersion=%REQ_VERSION%&id=%ITEM_ID%&version=%ITEM_VERSION%&'
    'maxAppVersion=%ITEM_MAXAPPVERSION%&status=%ITEM_STATUS%&appID=%APP_ID%&'
    'appVersion=%APP_VERSION%&appOS=%APP_OS%&appABI=%APP_ABI%&'
    'locale=%APP_LOCALE%&currentAppVersion=%CURRENT_APP_VERSION%&'
    'updateType=%UPDATE_TYPE%'
)


def get_filepath(fileorpath):
    """Get the actual file path of fileorpath if it's a FileUpload object."""
    if hasattr(fileorpath, 'path'):  # FileUpload
        return fileorpath.path
    return fileorpath


def get_file(fileorpath):
    """Get a file-like object, whether given a FileUpload object or a path."""
    if hasattr(fileorpath, 'path'):  # FileUpload
        return storage.open(fileorpath.path)
    if hasattr(fileorpath, 'name'):
        return fileorpath
    return storage.open(fileorpath)


def make_xpi(files):
    f = cStringIO()
    z = ZipFile(f, 'w')
    for path, data in files.items():
        z.writestr(path, data)
    z.close()
    f.seek(0)
    return f


def is_beta(version):
    """Return True if the version is believed to be a beta version."""
    return bool(amo.VERSION_BETA.search(version))


class Extractor(object):
    """Extract adon info from an install.rdf or package.json"""
    App = collections.namedtuple('App', 'appdata id min max')

    @classmethod
    def parse(cls, path):
        install_rdf = os.path.join(path, 'install.rdf')
        package_json = os.path.join(path, 'package.json')
        if os.path.exists(install_rdf):
            return RDFExtractor(path).data
        elif os.path.exists(package_json):
            return PackageJSONExtractor(package_json).parse()
        else:
            raise forms.ValidationError("No install.rdf or package.json found")


class PackageJSONExtractor(object):
    def __init__(self, path):
        self.path = path
        self.data = json.load(open(self.path))

    def get(self, key, default=None):
        return self.data.get(key, default)

    def apps(self):

        def find_appversion(app, version_req):
            """
            Convert an app and a package.json style version requirement to an
            `AppVersion`. This simply extracts the version number without the
            ><= requirement so it will not be accurate for version requirements
            that are not >=, <= or = to a version.

            >>> find_appversion(amo.APPS['firefox'], '>=33.0a1')
            <AppVersion: 33.0a1>
            """
            version = re.sub('>?=?<?', '', version_req)
            try:
                return AppVersion.objects.get(
                    application=app.id, version=version)
            except AppVersion.DoesNotExist:
                return None

        for engine, version in self.get('engines', {}).items():
            name = 'android' if engine == 'fennec' else engine
            app = amo.APPS.get(name)
            if app and app.guid in amo.APP_GUIDS:
                appversion = find_appversion(app, version)
                if appversion:
                    yield Extractor.App(
                        appdata=app, id=app.id, min=appversion, max=appversion)

    def parse(self):
        return {
            'guid': self.get('id') or self.get('name'),
            'type': amo.ADDON_EXTENSION,
            'name': self.get('title') or self.get('name'),
            'version': self.get('version'),
            'homepage': self.get('homepage'),
            'summary': self.get('description'),
            'no_restart': True,
            'apps': list(self.apps()),
        }


class RDFExtractor(object):
    """Extract add-on info from an install.rdf."""
    TYPES = {'2': amo.ADDON_EXTENSION, '4': amo.ADDON_THEME,
             '8': amo.ADDON_LPAPP, '64': amo.ADDON_DICT}
    manifest = u'urn:mozilla:install-manifest'

    def __init__(self, path):
        self.path = path
        install_rdf_path = os.path.join(path, 'install.rdf')
        self.rdf = rdflib.Graph().parse(open(install_rdf_path))
        self.package_type = None
        self.find_root()
        self.data = {
            'guid': self.find('id'),
            'type': self.find_type(),
            'name': self.find('name'),
            'version': self.find('version'),
            'homepage': self.find('homepageURL'),
            'summary': self.find('description'),
            'no_restart':
                self.find('bootstrap') == 'true' or self.find('type') == '64',
            'strict_compatibility': self.find('strictCompatibility') == 'true',
            'apps': self.apps(),
            'is_multi_package': self.package_type == '32',
        }

    def find_type(self):
        # If the extension declares a type that we know about, use
        # that.
        # https://developer.mozilla.org/en-US/Add-ons/Install_Manifests#type
        self.package_type = self.find('type')
        if self.package_type and self.package_type in self.TYPES:
            return self.TYPES[self.package_type]

        # Look for Complete Themes.
        if self.path.endswith('.jar') or self.find('internalName'):
            return amo.ADDON_THEME

        # Look for dictionaries.
        dic = os.path.join(self.path, 'dictionaries')
        if os.path.exists(dic) and glob.glob('%s/*.dic' % dic):
            return amo.ADDON_DICT

        # Consult <em:type>.
        return self.TYPES.get(self.package_type, amo.ADDON_EXTENSION)

    def uri(self, name):
        namespace = 'http://www.mozilla.org/2004/em-rdf'
        return rdflib.term.URIRef('%s#%s' % (namespace, name))

    def find_root(self):
        # If the install-manifest root is well-defined, it'll show up when we
        # search for triples with it.  If not, we have to find the context that
        # defines the manifest and use that as our root.
        # http://www.w3.org/TR/rdf-concepts/#section-triples
        manifest = rdflib.term.URIRef(self.manifest)
        if list(self.rdf.triples((manifest, None, None))):
            self.root = manifest
        else:
            self.root = self.rdf.subjects(None, self.manifest).next()

    def find(self, name, ctx=None):
        """Like $() for install.rdf, where name is the selector."""
        if ctx is None:
            ctx = self.root
        # predicate it maps to <em:{name}>.
        match = list(self.rdf.objects(ctx, predicate=self.uri(name)))
        # These come back as rdflib.Literal, which subclasses unicode.
        if match:
            return unicode(match[0])