#!/usr/bin/python3
#
# Copyright 2012 SUSE Linux
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

import argparse
import glob
import os
import re

from metaextract import utils as meta_utils
from packaging.version import parse
from packaging.requirements import Requirement


def _get_complete_requires(all_requires):
    """get a dict with all requirements from metaextract"""
    complete_requires = dict()

    if 'tests_require' in all_requires and all_requires['tests_require']:
        sanitized = sanitize_requirements(all_requires['tests_require'])
        # add the source of the requirement
        for key, val in sanitized.items():
            sanitized[key] = (val, 'tests')
        complete_requires.update(sanitized)

    if 'extras_require' in all_requires and all_requires['extras_require']:
        # extras_require is a dict of where the values are str or lists
        # and the key can be a marker (i.e. ":(python_version>='3.4')")
        extras_require = list()
        for key, val in all_requires['extras_require'].items():
            # check if the key is a marker
            marker = ""
            if key.startswith(":"):  # it's marker
                marker = ";%s" % key[1:]
            if isinstance(val, list):
                for v in val:
                    extras_require.append("%s%s" % (v, marker))
            else:
                extras_require.append("%s;%s" % (val, marker))

        sanitized = sanitize_requirements(extras_require)
        # add the source of the requirement
        for key, val in sanitized.items():
            sanitized[key] = (val, 'extras')
        complete_requires.update(sanitized)

    if 'install_requires' in all_requires and all_requires['install_requires']:
        sanitized = sanitize_requirements(all_requires['install_requires'])
        # add the source of the requirement
        for key, val in sanitized.items():
            sanitized[key] = (val, 'install')
        complete_requires.update(sanitized)

    return complete_requires


def parse_update_spec_file(specfile, contents, all_requires):
    found_requires = set()
    # complete dict of requires ('install', 'extras' and 'tests')
    complete_requires = _get_complete_requires(all_requires)

    for d in complete_requires:
        if re.search(r'^(Build)?Requires(?:\(.*\))?:\s+%s' % (d), contents,
                     flags=re.MULTILINE | re.IGNORECASE) is not None:
            found_requires.add(d)

        contents = re.sub(
            r'^(Requires(?:\(.*\))?:[ \t]+)(%s)(?:[ \t].*)?$' % (d),
            r'\g<1>\g<2>%s' %
            (" >= " + complete_requires[d][0] if complete_requires[d][0]
             else ""),
            contents, flags=re.MULTILINE | re.IGNORECASE)[:]

    if not specfile.endswith("-doc.spec"):
        spec_requires = set()
        for m in re.finditer(r"^Requires(?:\(.*\))?:[ \t]+python-([\w\d\.-]+)",
                             contents, flags=re.MULTILINE | re.IGNORECASE):
            spec_requires.add("python-%s" % m.group(1))

        complete_requires_keys = set(complete_requires)

        for d in complete_requires_keys.difference(found_requires):
            print("ERROR: %s is missing in %s (source: %s)" % (
                d, specfile, complete_requires[d][1]))

        for d in spec_requires.difference(complete_requires_keys):
            if d.lower() in all_requires:
                continue
            print("W: %s is not found in upstream" % d)

    return contents


def update_spec_files(all_requires):
    for specfile in glob.glob('./*.spec'):
        try:
            f = open(specfile, 'r+')
            contents = f.read()
            f.seek(0)
            f.truncate()
            contents = parse_update_spec_file(specfile, contents, all_requires)
            f.write(contents)
        finally:
            f.close()


def sanitize_requirements(requirements):
    d = dict()

    for l in requirements:
        # strip away comments
        l = re.sub('#.*', ' ', l)
        # strip away whitespace
        l = re.sub('[ \t]*', '', l)
        # ignore for now the requirements like '-e git://'
        l = re.sub(r'\s*-e.*', '', l)

        # in egg/requires.txt, there are sections starting with "[" for markers
        if not l or l.startswith("-f") or l.startswith("["):
            continue

        m = re.match('^http://tarballs.openstack.org/([^/]+)/', l)
        if m:
            l = m.group(1)

        if re.match('^python-(.*)client', l):
            l = re.sub(r'^python-(.+client)', r'\g<1>', l)

        r = Requirement(l)
        if r.marker:
            # TODO (toabctl): currently we hardcode python 2.7 and linux2
            # see https://www.python.org/dev/peps/pep-0508/#environment-markers
            marker_env = {'python_version': '2.7', 'sys_platform': 'linux'}
            if not r.marker.evaluate(environment=marker_env):
                continue

        pkg_name = r.name
        version_dep = None

        if r.specifier:
            # we want the lowest possible version
            # NOTE(toabctl): "min(r.specifier)" doesn't work.
            lowest = None
            for s in r.specifier:
                # we don't want a lowest version which is not allowed
                if s.operator == '!=' or s.operator == '<':
                    continue
                if not lowest or s.version < lowest.version:
                    lowest = s

            if lowest:
                version_dep = lowest.version

        # handle pre-release deps
        if version_dep:
            version_dep_python = parse(version_dep)
            if version_dep_python.is_prerelease:
                version_dep_rpm = version_dep_python.public
                # we need to add the 'x' in front of alpha/beta release because
                # in the python world, "1.1a10" > "1.1.dev10"
                # but in the rpm world, "1.1~a10" < "1.1~dev10"
                version_dep_rpm = version_dep_rpm.replace('a', '~xalpha')
                version_dep_rpm = version_dep_rpm.replace('b', '~xbeta')
                version_dep_rpm = version_dep_rpm.replace('rc', '~xrc')
                version_dep_rpm = version_dep_rpm.replace('.dev', '~dev')
                version_dep = version_dep_rpm

        d["python-%s" % (pkg_name)] = version_dep

    # ignore some requirements
    ignore_requires = (
        'python-discover',  # not needed since we only care about >= py27
        'python-coverage',
        'python-sphinx', 'python-setuptools_git',
        'python-setuptools',
        'python-setuptools-git', 'python-distribute',
        'python-pylint', 'python-hacking',
        'python-docutils', 'python-oslo.sphinx', 'python-oslosphinx',
        'python-hacking', 'python-qpid-python')

    real_requires = dict()
    for k in set(d).difference(ignore_requires):
        real_requires[k] = d[k]

    return real_requires


def get_tarball_candidate():
    # Try to find the tarball
    candidates = set()
    for d in os.listdir('.'):
        if '.tar.' in d:
            candidates.add(d)

    candidates = list(candidates)
    candidates.sort(key=lambda x: os.stat(
        os.path.join(os.getcwd(), x)).st_mtime, reverse=True)
    return candidates[0]


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Python Requires')
    parser.add_argument('--outdir',
                        help='osc service parameter that does nothing')
    args = parser.parse_args()

    filename = get_tarball_candidate()

    requires_meta = meta_utils.from_archive(filename)

    update_spec_files(requires_meta['data'])
