#!/usr/bin/env python
"""
This module is querying PyPI to check if the current version set to package is already present on PyPI.

Used during PR checks, to ensure that package version is changed.

Finishes with an VersionExists exception and a non-zero exit code if the version exists on PyPI.
"""
from __future__ import annotations

__version__ = '1.0.0rc0'

import json
from enum import IntFlag
from urllib.error import HTTPError
from urllib.request import urlopen

import os
import sys
from argparse import ArgumentParser
from importlib import invalidate_caches, import_module
from typing import List, Any, Optional, NamedTuple

from pkg_resources import safe_version


def check_unique(name: str, version: str, warehouse: str = 'https://pypi.org/pypi') -> None:
    try:
        response = urlopen(f'{warehouse}/{name}/json')
    except HTTPError as e:
        raise PypiError(name) from e
    data = json.loads(response.read())
    versions = set(data['releases'].keys())
    if version in versions:
        raise VersionExists(name, version)


def check_version_format(name: str, version: str) -> None:
    if safe_version(version) != version:
        raise InvalidVersionFormat(name, version)


def check_version_type(expected: VersionType, version: str) -> None:
    actual = VersionType.parse(version)
    if actual != expected:
        raise VersionTypeMismatch(version, actual, expected)


class VersionType(IntFlag):
    RELEASE = 0
    ALPHA = 1
    BETA = 2
    RC = 4
    DEV = 8

    @classmethod
    def parse(cls, version: str) -> VersionType:
        version_type = cls.RELEASE
        if 'a' in version:
            version_type |= cls.ALPHA
        if 'b' in version:
            version_type |= cls.BETA
        if 'rc' in version:
            version_type |= cls.RC
        if 'dev' in version:
            version_type |= cls.DEV
        return version_type


class InvalidRequirements(Exception):
    ...


class VersionTypeMismatch(Exception):
    def __init__(self, version: str, actual: VersionType, expected: VersionType):
        super().__init__(f'Package version {version} was specified to be {repr(expected)}, '
                         f'but actually it is {repr(actual)} ')


class InvalidVersionFormat(Exception):
    def __init__(self, name: str, version: str):
        super().__init__(f'Package "{name}" version "{version}" is not formatted according to PEP 440. '
                         f'Proper version may be "{safe_version(version)}. '
                         f'Read more: https://www.python.org/dev/peps/pep-0440/')


class VersionExists(Exception):
    def __init__(self, name: str, version: str):
        super().__init__(f'Package "{name}" with version "{version}" already exists on PyPI.{os.linesep}'
                         f'Change the "{name}.__version__" or "{name}.__init__.__version__" to fix this error.')


class PypiError(Exception):
    def __init__(self, name: str):
        super().__init__(f'Package "{name}" could not be fetched from PyPI. ')


parser = ArgumentParser(description='Check version of a Python package or module.')
parser.add_argument('module', type=str, help='the package/module with "__version__" defined')

parser.add_argument('-w', '--warehouse', type=str, default='https://pypi.org/pypi',
                    help='package index to use, default is "https://pypi.org/pypi"')

parser.add_argument('--alpha', action='store_true', default=False,
                    help='check that version is an alpha, e.g. 1.0.0a1')
parser.add_argument('--beta', action='store_true', default=False,
                    help='check that version is a beta, e.g. 1.0.0b2')
parser.add_argument('--rc', action='store_true', default=False,
                    help='check that version is a release candidate, e.g. 1.0.0rc')

parser.add_argument('--dev', action='store_true', default=False,
                    help='check that version is in development, e.g. 1.0.0.dev3')

parser.add_argument('--release', action='store_true', default=False,
                    help='check that version is a release without modifiers, e.g. 1.0.0')

parser.add_argument('--dry', action='store_true', default=False,
                    help='make no request to PyPI')


class Parameters(NamedTuple):
    warehouse: str
    package: str
    version: str
    expected_type: Optional[VersionType]
    dry_run: bool


def _parse_args(args: List[str]) -> Parameters:
    parameters = parser.parse_args(args)
    module_name = parameters.module
    module = _resolve_module(module_name)
    version_type = _parse_version_type(parameters)

    return Parameters(
        parameters.warehouse,
        module_name,
        module.__version__,
        version_type,
        parameters.dry
    )


def _parse_version_type(parameters):
    if not any([parameters.release, parameters.alpha, parameters.beta, parameters.rc, parameters.dev]):
        return None
    if parameters.release:
        if any([parameters.alpha, parameters.beta, parameters.rc, parameters.dev]):
            raise InvalidRequirements('--release cannot be combined with --alpha, --beta, --rc, or --dev')
        version_type = VersionType.RELEASE
    elif parameters.alpha:
        if any([parameters.beta, parameters.rc]):
            raise InvalidRequirements('--alpha, --beta and --rc cannot be combined')
        version_type = VersionType.ALPHA
    elif parameters.beta:
        if any([parameters.alpha, parameters.rc]):
            raise InvalidRequirements('--alpha, --beta and --rc cannot be combined')
        version_type = VersionType.BETA
    elif parameters.rc:
        version_type = VersionType.RC
    else:
        version_type = VersionType.RELEASE
    if parameters.dev:
        version_type |= VersionType.DEV
    return version_type


def _resolve_module(module_name: str) -> Any:
    """Black magic. Prevents loading a package from cv dependencies."""
    invalidate_caches()
    old_module = sys.modules.pop(module_name, None)
    module = import_module(module_name)
    if old_module:
        sys.modules[module_name] = old_module
    return module


def main(args):
    p = _parse_args(args)
    check_version_format(p.package, p.version)
    if p.expected_type is not None:
        check_version_type(p.expected_type, p.version)
    if not p.dry_run:
        check_unique(p.package, version=p.version, warehouse=p.warehouse)
    print(f'OK: {p.package} {p.version} is valid and not present on PyPI.')


if __name__ == '__main__':
    sys.path.insert(0, os.getcwd())
    main(sys.argv[1:])
