# All Rights Reserved.
#
#    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.

"""Tools for working with requirements lists.
"""

import logging
import os.path

import pkg_resources

from openstack_releases import gitutils
from openstack_releases import processutils
from openstack_releases import pythonutils
from openstack_releases import versionutils

LOG = logging.getLogger(__name__)


def find_bad_lower_bound_increases(workdir, repo,
                                   start_version, new_version, hash,
                                   report):
    if (new_version.split('.')[-1] == '0' or
            versionutils.looks_like_preversion(new_version)):
        # There is no need to look at the requirements so don't do the
        # extra work.
        return
    start_reqs = get_requirements_at_ref(workdir, repo, start_version)
    hash_reqs = get_requirements_at_ref(workdir, repo, hash)
    compare_lower_bounds(start_reqs, hash_reqs, report)


def compare_lower_bounds(start_reqs, hash_reqs, report):
    for (section, name), req in sorted(hash_reqs.items()):
        if section:
            display_name = '[{}]{}'.format(section, name)
        else:
            display_name = name

        if (section, name) not in start_reqs:
            report('New dependency {}'.format(display_name))

        else:
            old_req = start_reqs[(section, name)]
            old_min = get_min_specifier(old_req.specifier)
            new_min = get_min_specifier(req.specifier)

            if old_min is None and new_min is None:
                # No minimums are specified.
                continue

            if old_min is None:
                # There is no minimum on the old dependency but there
                # is now a new minimum.
                report(('Added minimum version for dependency {} of {} '
                        'without at least incrementing minor number').format(
                    display_name, new_min))
                continue

            if new_min is None:
                # There was a minimum but it has been removed.
                continue

            if old_min.version not in req.specifier:
                # The old minimum is not in the new spec.
                report(('Changed supported versions for dependency {} from {} '
                        'to {} without at least incrementing minor number').format(
                    display_name, old_req.specifier, req.specifier))


def get_min_specifier(specifier_set):
    "Find the specifier in the set that controls the lower bound."
    for s in specifier_set:
        if '>' in s.operator:
            return s


def get_requirements_at_ref(workdir, repo, ref):
    "Check out the repo at the ref and load the list of requirements."
    dest = gitutils.clone_repo(workdir, repo, ref=ref)
    processutils.check_call(['python', 'setup.py', 'sdist'], cwd=dest)
    sdist_name = pythonutils.get_sdist_name(workdir, repo)
    requirements_filename = os.path.join(
        dest, sdist_name + '.egg-info', 'requires.txt',
    )
    if os.path.exists(requirements_filename):
        with open(requirements_filename, 'r') as f:
            body = f.read()
    else:
        # The package has no dependencies.
        body = ''
    return parse_requirements(body)


def parse_requirements(body):
    """Given the requires.txt file for an sdist, parse it.

    Returns a dictionary mapping (section, pkg name) to the
    requirements specifier.

    Parses files that look like:

    pbr>=1.6
    keyring==7.3
    requests>=2.5.2
    PyYAML>=3.1.0
    yamlordereddictloader
    prompt_toolkit
    tqdm
    packaging>=15.2
    mwclient==0.8.1
    jsonschema>=2.6.0
    Jinja2>=2.6
    parawrap
    reno>=2.0.0
    sphinx>=1.6.2
    pyfiglet>=0.7.5

    [sphinxext]
    sphinx<1.6.1,>=1.5.1
    oslosphinx
    sphinxcontrib.datatemplates
    icalendar

    """
    requirements = {}
    section = ''
    for line in body.splitlines():
        # Ignore blank lines and comments
        if (not line.strip()) or line.startswith('#'):
            continue
        if line.startswith('['):
            section = line.lstrip('[').rstrip(']')
            continue

        try:
            parsed_req = pkg_resources.Requirement.parse(line)
        except ValueError:
            LOG.warning('failed to parse %r', line)
        else:
            requirements[(section, parsed_req.name)] = parsed_req

    return requirements