# built-in
from collections import defaultdict
from json import dumps as json_dumps
from logging import getLogger
from pathlib import Path
from typing import Optional

# external
import attr
from dephell_discover import Root as PackageRoot
from dephell_links import DirLink, FileLink, URLLink, VCSLink, parse_link
from dephell_setuptools import read_setup
from dephell_specifier import RangeSpecifier
from packaging.requirements import Requirement

# app
from ..constants import DOWNLOAD_FIELD, HOMEPAGE_FIELD
from ..controllers import DependencyMaker, Readme
from ..models import Author, EntryPoint, RootDependency
from .base import BaseConverter


try:
    from yapf.yapflib.style import CreateGoogleStyle
    from yapf.yapflib.yapf_api import FormatCode
except ImportError:
    FormatCode = None
try:
    from autopep8 import fix_code
except ImportError:
    fix_code = None


logger = getLogger('dephell.converters.setuppy')


TEMPLATE = """
# -*- coding: utf-8 -*-

# DO NOT EDIT THIS FILE!
# This file has been autogenerated by dephell <3
# https://github.com/dephell/dephell

try:
    from setuptools import setup
except ImportError:
    from distutils.core import setup

{readme}

setup(
    long_description=readme,
    {kwargs},
)
"""


@attr.s()
class SetupPyConverter(BaseConverter):
    lock = attr.ib(type=bool, default=False)

    def can_parse(self, path: Path, content: Optional[str] = None) -> bool:
        if isinstance(path, str):
            path = Path(path)
        if path.name == 'setup.py':
            return True
        if not content:
            return False
        if 'setuptools' not in content and 'distutils' not in content:
            return False
        return ('setup(' in content)

    def load(self, path) -> RootDependency:
        if isinstance(path, str):
            path = Path(path)
        path = self._make_source_path_absolute(path)
        self._resolve_path = path.parent

        data = read_setup(path=path, error_handler=logger.debug)
        root = RootDependency(
            raw_name=data['name'],
            version=data.get('version', '0.0.0'),
            package=PackageRoot(
                path=self.project_path or Path(),
                name=data['name'],
            ),

            description=data.get('description'),
            license=data.get('license'),

            keywords=tuple(data.get('keywords', ())),
            classifiers=tuple(data.get('classifiers', ())),
            platforms=tuple(data.get('platforms', ())),

            python=RangeSpecifier(data.get('python_requires')),
            readme=Readme.from_code(path=path),
        )

        # links
        fields = (
            (HOMEPAGE_FIELD, 'url'),
            (DOWNLOAD_FIELD, 'download_url'),
        )
        for key, name in fields:
            link = data.get(name)
            if link:
                root.links[key] = link

        # authors
        for name in ('author', 'maintainer'):
            author = data.get(name)
            if author:
                root.authors += (
                    Author(name=author, mail=data.get(name + '_email')),
                )

        # entrypoints
        entrypoints = []
        for group, content in data.get('entry_points', {}).items():
            for entrypoint in content:
                entrypoints.append(EntryPoint.parse(text=entrypoint, group=group))
        root.entrypoints = tuple(entrypoints)

        # dependency_links
        urls = dict()
        for url in data.get('dependency_links', ()):
            parsed = parse_link(url)
            name = parsed.name.split('-')[0]
            urls[name] = url

        # dependencies
        for req in data.get('install_requires', ()):
            req = Requirement(req)
            root.attach_dependencies(DependencyMaker.from_requirement(
                source=root,
                req=req,
                url=urls.get(req.name),
            ))

        # extras
        for extra, reqs in data.get('extras_require', {}).items():
            extra, marker = self._split_extra_and_marker(extra)
            envs = {extra} if extra == 'dev' else {'main', extra}
            for req in reqs:
                req = Requirement(req)
                root.attach_dependencies(DependencyMaker.from_requirement(
                    source=root,
                    req=req,
                    marker=marker,
                    envs=envs,
                ))

        return root

    def dumps(self, reqs, project: RootDependency, content=None) -> str:
        """
        https://setuptools.readthedocs.io/en/latest/setuptools.html#metadata
        """
        content = []
        content.append(('name', project.raw_name))
        content.append(('version', project.version))
        if project.description:
            content.append(('description', project.description))
        if project.python:
            content.append(('python_requires', str(project.python.peppify())))

        # links
        if project.links:
            content.append(('project_urls', project.links))

        # authors
        if project.authors:
            author = project.authors[0]
            content.append(('author', author.name))
            if author.mail:
                content.append(('author_email', author.mail))
        if len(project.authors) > 1:
            author = project.authors[1]
            content.append(('maintainer', author.name))
            if author.mail:
                content.append(('maintainer_email', author.mail))

        if project.license:
            content.append(('license', project.license))
        if project.keywords:
            content.append(('keywords', ' '.join(project.keywords)))
        if project.classifiers:
            content.append(('classifiers', list(project.classifiers)))
        if project.platforms:
            content.append(('platforms', project.platforms))
        if project.entrypoints:
            entrypoints = defaultdict(list)
            for entrypoint in project.entrypoints:
                entrypoints[entrypoint.group].append(str(entrypoint))
            content.append(('entry_points', entrypoints))

        # packages, package_data
        content.append(('packages', sorted(str(p) for p in project.package.packages)))
        if project.package.package_dir:
            content.append(('package_dir', project.package.package_dir))
        data = defaultdict(list)
        for rule in project.package.data:
            data[rule.module].append(rule.relative)
        data = {package: sorted(paths) for package, paths in data.items()}
        content.append(('package_data', data))

        # depedencies
        reqs_list = [self._format_req(req=req) for req in reqs if not req.main_envs]
        content.append(('install_requires', reqs_list))

        # dependency_links
        links = []
        for req in reqs:
            if req.dep.link is not None:
                links.append(self._format_link(req=req))
        if links:
            content.append(('dependency_links', links))

        # extras
        extras = defaultdict(list)
        for req in reqs:
            if req.main_envs:
                formatted = self._format_req(req=req)
                for env in req.main_envs:
                    extras[env].append(formatted)
        if extras:
            content.append(('extras_require', extras))

        if project.readme is not None:
            readme = project.readme.to_rst().as_code()
        else:
            readme = "readme = ''"

        content = ',\n    '.join(
            '{}={!s}'.format(name, json_dumps(value, sort_keys=True))
            if isinstance(value, dict) else '{}={!r}'.format(name, value)
            for name, value in content)
        content = TEMPLATE.format(kwargs=content, readme=readme)

        # beautify
        if FormatCode is not None:
            content, _changed = FormatCode(content, style_config=CreateGoogleStyle())
        if fix_code is not None:
            content = fix_code(content)

        return content

    # private methods

    @staticmethod
    def _format_req(req) -> str:
        line = req.raw_name
        if req.extras:
            line += '[{extras}]'.format(extras=','.join(req.extras))
        if req.version:
            line += req.version
        if req.markers:
            line += '; ' + req.markers
        return line

    @staticmethod
    def _format_link(req) -> str:
        link = req.dep.link
        egg = '#egg=' + req.name
        if req.release:
            egg += '-' + str(req.release.version)

        if isinstance(link, (FileLink, DirLink)):
            return link.short

        if isinstance(link, VCSLink):
            result = link.vcs + '+' + link.short
            if link.rev:
                result += '@' + link.rev
            return result + egg

        if isinstance(link, URLLink):
            return link.short + egg

        raise ValueError('invalid link for {}'.format(req.name))