#!/usr/bin/env python
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

import re
import sys


def main():
    constraints_path = 'test/lib/ansible_test/_data/requirements/constraints.txt'

    requirements = {}

    for path in sys.argv[1:] or sys.stdin.read().splitlines():
        if path == 'test/lib/ansible_test/_data/requirements/sanity.import-plugins.txt':
            # This file is an exact copy of requirements.txt that is used in the import
            # sanity test.  There is a code-smell test which ensures that the two files
            # are identical, and it is only used inside an empty venv, so we can ignore
            # it here.
            continue
        with open(path, 'r') as path_fd:
            requirements[path] = parse_requirements(path_fd.read().splitlines())

    frozen_sanity = {}
    non_sanity_requirements = set()

    for path, requirements in requirements.items():
        for lineno, line, requirement in requirements:
            if not requirement:
                print('%s:%d:%d: cannot parse requirement: %s' % (path, lineno, 1, line))
                continue

            name = requirement.group('name').lower()
            raw_constraints = requirement.group('constraints')
            raw_markers = requirement.group('markers')
            constraints = raw_constraints.strip()
            markers = raw_markers.strip()
            comment = requirement.group('comment')

            is_sanity = path.startswith('test/lib/ansible_test/_data/requirements/sanity.') or path.startswith('test/sanity/code-smell/')
            is_pinned = re.search('^ *== *[0-9.]+$', constraints)
            is_constraints = path == constraints_path

            if is_sanity:
                sanity = frozen_sanity.setdefault(name, [])
                sanity.append((path, lineno, line, requirement))
            elif not is_constraints:
                non_sanity_requirements.add(name)

            if constraints and not is_constraints:
                allow_constraints = 'sanity_ok' in comment

                if is_sanity and is_pinned and not markers:
                    allow_constraints = True  # sanity tests can use frozen requirements without markers

                if not allow_constraints:
                    if is_sanity:
                        # sanity test requirements which need constraints should be frozen to maintain consistent test results
                        # use of anything other than frozen constraints will make evaluation of conflicts extremely difficult
                        print('%s:%d:%d: sanity test constraint (%s%s) must be frozen (use `==`)' % (path, lineno, 1, name, raw_constraints))
                    else:
                        # keeping constraints for tests other than sanity tests in one file helps avoid conflicts
                        print('%s:%d:%d: put the constraint (%s%s) in `%s`' % (path, lineno, 1, name, raw_constraints, constraints_path))

    for name, requirements in frozen_sanity.items():
        for req in requirements:
            if name in non_sanity_requirements and req[3].group('constraints').strip():
                print('%s:%d:%d: sanity constraint (%s) for package `%s` is not allowed because `%s` is used outside sanity tests' % (
                    req[0], req[1], req[3].start('constraints') + 1, req[3].group('constraints'), name, name))

        if len(set(req[3].group('constraints').strip() for req in requirements)) != 1:
            for req in requirements:
                print('%s:%d:%d: sanity constraint (%s) does not match others for package `%s`' % (
                    req[0], req[1], req[3].start('constraints') + 1, req[3].group('constraints'), name))


def parse_requirements(lines):
    # see https://www.python.org/dev/peps/pep-0508/#names
    pattern = re.compile(r'^(?P<name>[A-Z0-9][A-Z0-9._-]*[A-Z0-9]|[A-Z0-9])(?P<extras> *\[[^]]*])?(?P<constraints>[^;#]*)(?P<markers>[^#]*)(?P<comment>.*)$',
                         re.IGNORECASE)

    matches = [(lineno, line, pattern.search(line)) for lineno, line in enumerate(lines, start=1)]
    requirements = []

    for lineno, line, match in matches:
        if not line.strip():
            continue

        if line.strip().startswith('#'):
            continue

        if line.startswith('git+https://'):
            continue  # hack to ignore git requirements

        requirements.append((lineno, line, match))

    return requirements


if __name__ == '__main__':
    main()
