import abc
import os
import tempfile
from contextlib import contextmanager
from typing import List, Optional, Generic, TypeVar

from dbt.clients import system
from dbt.contracts.project import ProjectPackageMetadata
from dbt.logger import GLOBAL_LOGGER as logger

DOWNLOADS_PATH = None


def get_downloads_path():
    return DOWNLOADS_PATH


@contextmanager
def downloads_directory():
    global DOWNLOADS_PATH
    remove_downloads = False
    # the user might have set an environment variable. Set it to that, and do
    # not remove it when finished.
    if DOWNLOADS_PATH is None:
        DOWNLOADS_PATH = os.getenv('DBT_DOWNLOADS_DIR')
        remove_downloads = False
    # if we are making a per-run temp directory, remove it at the end of
    # successful runs
    if DOWNLOADS_PATH is None:
        DOWNLOADS_PATH = tempfile.mkdtemp(prefix='dbt-downloads-')
        remove_downloads = True

    system.make_directory(DOWNLOADS_PATH)
    logger.debug("Set downloads directory='{}'".format(DOWNLOADS_PATH))

    yield DOWNLOADS_PATH

    if remove_downloads:
        system.rmtree(DOWNLOADS_PATH)
        DOWNLOADS_PATH = None


class BasePackage(metaclass=abc.ABCMeta):
    @abc.abstractproperty
    def name(self) -> str:
        raise NotImplementedError

    def all_names(self) -> List[str]:
        return [self.name]

    @abc.abstractmethod
    def source_type(self) -> str:
        raise NotImplementedError


class PinnedPackage(BasePackage):
    def __init__(self) -> None:
        self._cached_metadata: Optional[ProjectPackageMetadata] = None

    def __str__(self) -> str:
        version = self.get_version()
        if not version:
            return self.name

        return '{}@{}'.format(self.name, version)

    @abc.abstractmethod
    def get_version(self) -> Optional[str]:
        raise NotImplementedError

    @abc.abstractmethod
    def _fetch_metadata(self, project, renderer):
        raise NotImplementedError

    @abc.abstractmethod
    def install(self, project):
        raise NotImplementedError

    @abc.abstractmethod
    def nice_version_name(self):
        raise NotImplementedError

    def fetch_metadata(self, project, renderer):
        if not self._cached_metadata:
            self._cached_metadata = self._fetch_metadata(project, renderer)
        return self._cached_metadata

    def get_project_name(self, project, renderer):
        metadata = self.fetch_metadata(project, renderer)
        return metadata.name

    def get_installation_path(self, project, renderer):
        dest_dirname = self.get_project_name(project, renderer)
        return os.path.join(project.modules_path, dest_dirname)

    def get_subdirectory(self):
        return None


SomePinned = TypeVar('SomePinned', bound=PinnedPackage)
SomeUnpinned = TypeVar('SomeUnpinned', bound='UnpinnedPackage')


class UnpinnedPackage(Generic[SomePinned], BasePackage):
    @abc.abstractclassmethod
    def from_contract(cls, contract):
        raise NotImplementedError

    @abc.abstractmethod
    def incorporate(self: SomeUnpinned, other: SomeUnpinned) -> SomeUnpinned:
        raise NotImplementedError

    @abc.abstractmethod
    def resolved(self) -> SomePinned:
        raise NotImplementedError
