# built-in
import re
from copy import copy
from logging import getLogger
from typing import List, Optional, Union

# external
from dephell_links import UnknownLink, VCSLink, parse_link
from dephell_markers import Markers
from dephell_specifier import GitSpecifier
from packaging.requirements import Requirement as PackagingRequirement

# app
from ..models.constraint import Constraint
from ..models.dependency import Dependency
from ..models.extra_dependency import ExtraDependency
from ..models.marker_tracker import MarkerTracker
from ..repositories import get_repo


logger = getLogger('dephell.controllers')
# regex for names generated by pipenv
rex_hash = re.compile(r'[a-f0-9]{7}')
rex_vers = re.compile(r'[a-z_-]+\-[0-9.]+')


class DependencyMaker:
    dep_class = Dependency
    extra_class = ExtraDependency

    @classmethod
    def from_requirement(cls, source, req, *, url=None, envs=None, marker: Union[Markers, str] = None,
                         editable=False) -> List[Union[Dependency, ExtraDependency]]:
        if type(req) is str:
            req = PackagingRequirement(req)
        # https://github.com/pypa/packaging/blob/master/packaging/requirements.py
        link = parse_link(url or req.url)
        # make constraint
        constraint = Constraint(source, req.specifier)
        if isinstance(link, VCSLink) and link.rev:
            constraint._specs[source.name] = GitSpecifier()

        if isinstance(marker, Markers):
            marker = copy(marker)
        else:
            marker = Markers(marker)
        if req.marker is not None:
            # some libs uses `in` for python_version,
            # but dephell_markers isn't ready for this
            try:
                marker &= Markers(req.marker)
            except ValueError:
                logger.warning('cannot parse marker', extra=dict(marker=req.marker))

        if envs is None:
            envs = {'main'}
        envs.update(marker.extract('extra'))

        default_repo = None
        if source.repo and source.repo.propagate:
            default_repo = source.repo

        name = req.name
        # drop version from the end
        if rex_vers.fullmatch(name):
            name = name.rsplit('-', maxsplit=1)[0]

        base_dep = cls.dep_class(
            raw_name=name,
            constraint=constraint,
            repo=get_repo(link, default=default_repo),
            link=link,
            marker=MarkerTracker().apply(source=source, markers=marker),
            editable=editable,
            envs=envs,
        )
        deps = [base_dep]
        if req.extras:
            for extra in req.extras:
                deps.append(cls.extra_class.from_dep(dep=base_dep, extra=extra))
        return deps

    @classmethod
    def from_params(cls, *, raw_name: str, constraint,
                    source: Dependency, url: Optional[str] = None,
                    repo=None, marker: Union[Markers, str] = None,
                    extras: Optional[List[str]] = None, envs=None,
                    **kwargs) -> List[Union[Dependency, ExtraDependency]]:

        # make link
        if not url or isinstance(url, str):
            link = parse_link(url)
        else:
            link = url
        if link and not isinstance(link, UnknownLink):
            if link.name and rex_hash.fullmatch(raw_name):
                if not link.name.startswith(raw_name):
                    raw_name = link.name

        # make constraint
        if isinstance(constraint, str):
            constraint = Constraint(source, constraint)
            if isinstance(link, VCSLink) and link.rev:
                constraint._specs[source.name] = GitSpecifier()

        # make repo
        if repo is None:
            default_repo = None
            if source.repo and source.repo.propagate:
                default_repo = source.repo
            repo = get_repo(link, default=default_repo)

        # make marker
        if isinstance(marker, Markers):
            marker = copy(marker)
        else:
            marker = Markers(marker)

        # make envs
        if envs is None:
            envs = {'main'}
        envs.update(marker.extract('extra'))

        base_dep = cls.dep_class(
            link=link,
            repo=repo,
            raw_name=raw_name,
            constraint=constraint,
            marker=MarkerTracker().apply(source=source, markers=marker),
            envs=envs,
            **kwargs,
        )
        deps = [base_dep]
        if extras:
            for extra in extras:
                deps.append(cls.extra_class.from_dep(dep=base_dep, extra=extra))
        return deps

    @classmethod
    def from_root(cls, dep, root) -> List[Union[Dependency, ExtraDependency]]:
        return cls.from_params(
            raw_name=dep.raw_name,
            constraint='==' + dep.version,
            source=root,
            locations={dep.package.packages[0].path},

            authors=dep.authors,
            classifiers=dep.classifiers,
            description=dep.description,
            license=dep.license,
            links=dep.links,
        )
