import logging
import os
import re
import sys
try:
    import urllib2 as urllib
except ImportError:
    import urllib
    from urllib import request
    urllib.Request = request.Request
    urllib.ProxyHandler = request.ProxyHandler
    urllib.build_opener = request.build_opener
    urllib.install_opener = request.install_opener
try:
    import xmlrpclib
except ImportError:
    import xmlrpc.client as xmlrpclib

try:
    import dnf
except ImportError:
    dnf = None

import jinja2
import pprint

from pyp2rpm import exceptions
from pyp2rpm import filters
from pyp2rpm import metadata_extractors
from pyp2rpm import name_convertor
from pyp2rpm import package_getters
from pyp2rpm import settings

logger = logging.getLogger(__name__)


class Convertor(object):
    """Object that takes care of the actual process of converting
    the package.
    """

    def __init__(self, package=None, version=None, prerelease=False,
                 save_dir=None,
                 template=settings.DEFAULT_TEMPLATE,
                 distro=settings.DEFAULT_DISTRO,
                 base_python_version=settings.DEFAULT_PYTHON_VERSION,
                 python_versions=[],
                 rpm_name=None, proxy=None, venv=True, autonc=False):
        self.package = package
        self.version = version
        self.prerelease = prerelease
        self.save_dir = save_dir
        self.base_python_version = base_python_version
        self.python_versions = list(python_versions)
        self.template = template
        self.distro = distro
        if not self.template.endswith('.spec'):
            self.template = '{0}.spec'.format(self.template)
        self.rpm_name = rpm_name
        self.proxy = proxy
        self.venv = venv
        self.autonc = autonc
        self.pypi = True
        suffix = os.path.splitext(self.package)[1]
        if (os.path.exists(self.package)
                and suffix in settings.ARCHIVE_SUFFIXES
                and not os.path.isdir(self.package)):
            self.pypi = False

    @property
    def template_base_py_ver(self):
        """Return default base python version for chosen template. """
        return settings.DEFAULT_PYTHON_VERSIONS[self.distro][0]

    @property
    def template_py_vers(self):
        """Return default python versions for chosen template. """
        return settings.DEFAULT_PYTHON_VERSIONS[self.distro][1:]

    def merge_versions(self, data):
        """Merges python versions specified in command lines options with
        extracted versions, checks if some of the versions is not > 2 if EPEL6
        template will be used. attributes base_python_version and
        python_versions contain values specified by command line options or
        default values, data.python_versions contains extracted data.
        """
        if self.distro == "epel6":
            # if user requested version greater than 2, writes error message
            # and exits
            requested_versions = self.python_versions
            if self.base_python_version:
                requested_versions += [self.base_python_version]
            if any(int(ver[0]) > 2 for ver in requested_versions):
                sys.stderr.write(
                    "Invalid version, major number of python version for "
                    "EPEL6 spec file must not be greater than 2.\n")
                sys.exit(1)
            # if version greater than 2 were extracted it is removed
            data.python_versions = [
                ver for ver in data.python_versions if not int(ver[0]) > 2]

        # Set python versions from default values in settings.
        base_version, additional_versions = (
            self.template_base_py_ver, self.template_py_vers)

        # Sync default values with extracted versions from PyPI classifiers.
        if data.python_versions:
            if base_version not in data.python_versions:
                base_version = data.python_versions[0]

            additional_versions = [
                v for v in additional_versions if v in data.python_versions]

        # Override default values with those set from command line if any.
        if self.base_python_version:
            base_version = self.base_python_version
        if self.python_versions:
            additional_versions = self.python_versions
        # Ensure there are no duplicate versions
        additional_versions = [
            v for v in additional_versions if v != base_version]

        data.base_python_version = base_version
        data.python_versions = additional_versions

    def convert(self):
        """Returns RPM SPECFILE.
        Returns:
            rendered RPM SPECFILE.
        """
        # move file into position
        try:
            local_file = self.getter.get()
        except (exceptions.NoSuchPackageException, OSError) as e:
            logger.error(
                "Failed and exiting:", exc_info=True)
            logger.info("Pyp2rpm failed. See log for more info.")

            sys.exit(e)

        # save name and version from the file (rewrite if set previously)
        self.name, self.version = self.getter.get_name_version()

        self.local_file = local_file
        data = self.metadata_extractor.extract_data(self.client)
        logger.debug("Extracted metadata:")
        logger.debug(pprint.pformat(data.data))
        self.merge_versions(data)

        jinja_env = jinja2.Environment(loader=jinja2.ChoiceLoader([
            jinja2.FileSystemLoader(['/']),
            jinja2.PackageLoader('pyp2rpm', 'templates'), ]))

        for filter in filters.__all__:
            jinja_env.filters[filter.__name__] = filter

        try:
            jinja_template = jinja_env.get_template(
                os.path.abspath(self.template))
        except jinja2.exceptions.TemplateNotFound:
            # absolute path not found => search in default template dir
            logger.warning('Template: {0} was not found in {1} using default '
                           'template dir.'.format(
                               self.template, os.path.abspath(self.template)))

            jinja_template = jinja_env.get_template(self.template)
            logger.info('Using default template: {0}.'.format(self.template))

        ret = jinja_template.render(data=data, name_convertor=name_convertor)
        return re.sub(r'[ \t]+\n', "\n", ret)

    @property
    def getter(self):
        """Returns an instance of proper PackageGetter subclass. Always
        returns the same instance.

        Returns:
            Instance of the proper PackageGetter subclass according to
            provided argument.
        Raises:
            NoSuchSourceException if source to get the package from is unknown
            NoSuchPackageException if the package is unknown on PyPI
        """
        if not hasattr(self, '_getter'):
            if not self.pypi:
                self._getter = package_getters.LocalFileGetter(
                    self.package,
                    self.save_dir)
            else:
                logger.debug(
                    '{0} does not exist as local file trying PyPI.'.format(
                        self.package))
                self._getter = package_getters.PypiDownloader(
                    self.client,
                    self.package,
                    self.version,
                    self.prerelease,
                    self.save_dir)

        return self._getter

    @property
    def local_file(self):
        """Returns an local_file attribute needed for metadata_extractor.

        *Must* be set before calling metadata_extractor attribute.

        Returns:
            Full path of local/downloaded file
        """
        return self._local_file

    @local_file.setter
    def local_file(self, value):
        """Setter for local_file attribute
        """
        self._local_file = value

    @property
    def name_convertor(self):
        if not hasattr(self, '_name_convertor'):
            name_convertor.NameConvertor.distro = self.distro
            if self.autonc or (self.autonc is None and
                (self.distro == 'fedora' or
                 self.distro == 'mageia')):
                logger.debug("Using AutoProvidesNameConvertor to convert "
                             "names of the packages.")
                self._name_convertor = name_convertor.AutoProvidesNameConvertor(
                    self.distro)
            elif dnf is None:
                logger.warning("Dnf module not found, please dnf install "
                               "python{0}-dnf to improve accuracy of name "
                               "conversion.".format(sys.version[0]))
                logger.debug(
                    "Using NameConvertor to convert names of the packages.")
                self._name_convertor = name_convertor.NameConvertor(
                    self.distro)
            else:
                logger.debug("Using DandifiedNameConvertor to convert names "
                             "of the packages.")
                self._name_convertor = name_convertor.DandifiedNameConvertor(
                    self.distro)
        return self._name_convertor

    @property
    def metadata_extractor(self):
        """Returns an instance of proper MetadataExtractor subclass.
        Always returns the same instance.

        Returns:
            The proper MetadataExtractor subclass according to local file
            suffix.
        """
        if not hasattr(self, '_local_file'):
            raise AttributeError("local_file attribute must be set before "
                                 "calling metadata_extractor")
        if not hasattr(self, '_metadata_extractor'):
            if self.local_file.endswith('.whl'):
                logger.info("Getting metadata from wheel using "
                            "WheelMetadataExtractor.")
                extractor_cls = metadata_extractors.WheelMetadataExtractor
            else:
                logger.info("Getting metadata from setup.py using "
                            "SetupPyMetadataExtractor.")
                extractor_cls = metadata_extractors.SetupPyMetadataExtractor

            base_python_version = (
                self.base_python_version or self.template_base_py_ver)

            self._metadata_extractor = extractor_cls(
                self.local_file,
                self.name,
                self.name_convertor,
                self.version,
                self.rpm_name,
                self.venv,
                self.distro,
                base_python_version)

        return self._metadata_extractor

    @property
    def client(self):
        """XMLRPC client for PyPI. Always returns the same instance.

        If the package is provided as a path to compressed source file,
        PyPI will not be used and the client will not be instantiated.

        Returns:
            XMLRPC client for PyPI or None.
        """
        if self.proxy:
            proxyhandler = urllib.ProxyHandler({"http": self.proxy})
            opener = urllib.build_opener(proxyhandler)
            urllib.install_opener(opener)
            transport = ProxyTransport()
        if not hasattr(self, '_client'):
            transport = None
            if self.pypi:
                if self.proxy:
                    logger.info('Using provided proxy: {0}.'.format(
                        self.proxy))
                self._client = xmlrpclib.ServerProxy(settings.PYPI_URL,
                                                     transport=transport)
                self._client_set = True
            else:
                self._client = None

        return self._client


class ProxyTransport(xmlrpclib.Transport):
    """This class serves as Proxy Transport for XMLRPC server."""

    def request(self, host, handler, request_body, verbose):
        self.verbose = verbose
        url = 'http://{0}{1}'.format(host, handler)
        request = urllib.Request(url)
        request.add_data(request_body)
        request.add_header("User-Agent", self.user_agent)
        request.add_header("Content-Type", "text/html")
        f = urllib.urlopen(request)
        return self.parse_response(f)