import os

from requests.exceptions import RequestException

from conans.client.graph.graph import (RECIPE_DOWNLOADED, RECIPE_INCACHE, RECIPE_NEWER,
                                       RECIPE_NOT_IN_REMOTE, RECIPE_NO_REMOTE, RECIPE_UPDATEABLE,
                                       RECIPE_UPDATED, RECIPE_EDITABLE)
from conans.client.output import ScopedOutput
from conans.client.recorder.action_recorder import INSTALL_ERROR_MISSING, INSTALL_ERROR_NETWORK
from conans.client.remover import DiskRemover
from conans.errors import ConanException, NotFoundException
from conans.model.manifest import FileTreeManifest
from conans.util.tracer import log_recipe_got_from_local_cache


class ConanProxy(object):
    def __init__(self, cache, output, remote_manager):
        # collaborators
        self._cache = cache
        self._out = output
        self._remote_manager = remote_manager
        self._registry = cache.registry

    def get_recipe(self, ref, check_updates, update, remote_name, recorder):
        if self._cache.installed_as_editable(ref):
            conanfile_path = self._cache.conanfile(ref)
            status = RECIPE_EDITABLE
            # TODO: log_recipe_got_from_editable(reference)
            # TODO: recorder.recipe_fetched_as_editable(reference)
            return conanfile_path, status, None, ref

        with self._cache.conanfile_write_lock(ref):
            result = self._get_recipe(ref, check_updates, update, remote_name, recorder)
            conanfile_path, status, remote, new_ref = result

            if status not in (RECIPE_DOWNLOADED, RECIPE_UPDATED):
                log_recipe_got_from_local_cache(new_ref)
                recorder.recipe_fetched_from_cache(new_ref)

        return conanfile_path, status, remote, new_ref

    def _get_recipe(self, ref, check_updates, update, remote_name, recorder):
        output = ScopedOutput(str(ref), self._out)
        # check if it is in disk
        conanfile_path = self._cache.conanfile(ref)

        # NOT in disk, must be retrieved from remotes
        if not os.path.exists(conanfile_path):
            remote, new_ref = self._download_recipe(ref, output, remote_name, recorder)
            status = RECIPE_DOWNLOADED
            return conanfile_path, status, remote, new_ref

        remote = self._registry.refs.get(ref)
        named_remote = self._registry.remotes.get(remote_name) if remote_name else None
        update_remote = named_remote or remote

        check_updates = check_updates or update
        # Recipe exists in disk, but no need to check updates
        cur_revision = self._cache.package_layout(ref).recipe_revision()
        requested_different_revision = (ref.revision is not None) and cur_revision != ref.revision
        if requested_different_revision and not check_updates:
            raise NotFoundException("The recipe in the local cache doesn't match the specified "
                                    "revision. Use '--update' to check in the remote.")

        if not requested_different_revision:
            if not check_updates:
                status = RECIPE_INCACHE
                ref = ref.copy_with_rev(cur_revision)
                return conanfile_path, status, remote, ref

            if not update_remote:
                status = RECIPE_NO_REMOTE
                ref = ref.copy_with_rev(cur_revision)
                return conanfile_path, status, None, ref
        else:  # Requested different revision and with --update
            remote, new_ref = self._download_recipe(ref, output, remote_name, recorder)
            status = RECIPE_DOWNLOADED
            return conanfile_path, status, remote, new_ref

        try:  # get_recipe_manifest can fail, not in server
            upstream_manifest, ref = self._remote_manager.get_recipe_manifest(ref, update_remote)
        except NotFoundException:
            status = RECIPE_NOT_IN_REMOTE
            ref = ref.copy_with_rev(cur_revision)
            return conanfile_path, status, update_remote, ref

        export = self._cache.export(ref)
        read_manifest = FileTreeManifest.load(export)
        if upstream_manifest != read_manifest:
            if upstream_manifest.time > read_manifest.time:
                if update:
                    DiskRemover().remove_recipe(self._cache.package_layout(ref), output=output)
                    output.info("Retrieving from remote '%s'..." % update_remote.name)
                    self._download_recipe(ref, output, update_remote.name, recorder)
                    self._registry.refs.set(ref, update_remote.name)
                    status = RECIPE_UPDATED
                    return conanfile_path, status, update_remote, ref
                else:
                    status = RECIPE_UPDATEABLE
            else:
                status = RECIPE_NEWER
        else:
            status = RECIPE_INCACHE

        ref = ref.copy_with_rev(cur_revision)
        return conanfile_path, status, update_remote, ref

    def _download_recipe(self, ref, output, remote_name, recorder):

        def _retrieve_from_remote(_ref, the_remote):
            output.info("Trying with '%s'..." % the_remote.name)
            # If incomplete, resolve the latest in server
            _ref = self._remote_manager.get_recipe(_ref, the_remote)
            output.info("Downloaded recipe revision %s" % _ref.revision)
            self._registry.refs.set(_ref, the_remote.name)
            recorder.recipe_downloaded(ref, the_remote.url)
            return _ref

        if remote_name:
            output.info("Retrieving from server '%s' " % remote_name)
            remote = self._registry.remotes.get(remote_name)
        else:
            remote = self._registry.refs.get(ref)
            if remote:
                output.info("Retrieving from predefined remote '%s'" % remote.name)

        if remote:
            try:
                new_ref = _retrieve_from_remote(ref, remote)
                return remote, new_ref
            except NotFoundException:
                msg = "%s was not found in remote '%s'" % (str(ref), remote.name)
                recorder.recipe_install_error(ref, INSTALL_ERROR_MISSING,
                                              msg, remote.url)
                raise NotFoundException(msg)
            except RequestException as exc:
                recorder.recipe_install_error(ref, INSTALL_ERROR_NETWORK,
                                              str(exc), remote.url)
                raise exc

        output.info("Not found in local cache, looking in remotes...")
        remotes = self._registry.remotes.list
        if not remotes:
            raise ConanException("No remote defined")
        for remote in remotes:
            try:
                new_ref = _retrieve_from_remote(ref, remote)
                return remote, new_ref
            # If not found continue with the next, else raise
            except NotFoundException:
                pass
        else:
            msg = "Unable to find '%s' in remotes" % ref.full_repr()
            recorder.recipe_install_error(ref, INSTALL_ERROR_MISSING,
                                          msg, None)
            raise NotFoundException(msg)

    def search_remotes(self, pattern, remote_name):
        if remote_name:
            remote = self._registry.remotes.get(remote_name)
            search_result = self._remote_manager.search_recipes(remote, pattern, ignorecase=False)
            return search_result

        for remote in self._registry.remotes.list:
            search_result = self._remote_manager.search_recipes(remote, pattern, ignorecase=False)
            if search_result:
                return search_result
