# -*- coding: utf-8 -*-
# providerbootstrapper.py
# Copyright (C) 2013 LEAP
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""
Provider bootstrapping
"""
import logging
import socket
import os
import sys

import requests

from leap.bitmask import provider
from leap.bitmask import util
from leap.bitmask.config import flags
from leap.bitmask.config.providerconfig import ProviderConfig, MissingCACert
from leap.bitmask.provider import get_provider_path
from leap.bitmask.provider.pinned import PinnedProviders
from leap.bitmask.services.abstractbootstrapper import AbstractBootstrapper
from leap.bitmask.util.constants import REQUEST_TIMEOUT
from leap.bitmask.util.request_helpers import get_content
from leap.common import ca_bundle
from leap.common.certs import get_digest
from leap.common.check import leap_assert, leap_assert_type, leap_check
from leap.common.files import check_and_fix_urw_only, get_mtime, mkdir_p

logger = logging.getLogger(__name__)


class UnsupportedProviderAPI(Exception):
    """
    Raised when attempting to use a provider with an incompatible API.
    """
    pass


class UnsupportedClientVersionError(Exception):
    """
    Raised when attempting to use a provider with an older
    client than supported.
    """
    pass


class WrongFingerprint(Exception):
    """
    Raised when a fingerprint comparison does not match.
    """
    pass


class ProviderBootstrapper(AbstractBootstrapper):
    """
    Given a provider URL performs a series of checks and emits signals
    after they are passed.
    If a check fails, the subsequent checks are not executed
    """

    MIN_CLIENT_VERSION = 'x-minimum-client-version'

    def __init__(self, signaler=None, bypass_checks=False):
        """
        Constructor for provider bootstrapper object

        :param signaler: Signaler object used to receive notifications
                         from the backend
        :type signaler: Signaler
        :param bypass_checks: Set to true if the app should bypass
                              first round of checks for CA
                              certificates at bootstrap
        :type bypass_checks: bool
        """
        AbstractBootstrapper.__init__(self, signaler, bypass_checks)

        self._domain = None
        self._provider_config = None
        self._download_if_needed = False
        if signaler is not None:
            self._cancel_signal = signaler.prov_cancelled_setup

    @property
    def verify(self):
        """
        Verify parameter for requests.

        :returns: either False, if checks are skipped, or the
                  path to the ca bundle.
        :rtype: bool or str
        """
        if self._bypass_checks:
            return False

        cert = flags.CA_CERT_FILE
        if cert is not None:
            verify = cert
        else:
            verify = ca_bundle.where()

        return verify

    def _check_name_resolution(self):
        """
        Checks that the name resolution for the provider name works
        """
        leap_assert(self._domai
n, "Cannot check DNS without a domain")
        logger.debug("Checking name resolution for %r" % (self._domain))

        # We don't skip this check, since it's basic for the whole
        # sys
tem to work
        # err --- but we can do it after a failure, to diagnose what went
        # wrong. Right now we're just adding connection overhead. -- kali
        socket.gethostbyname(self._domain.encode('idna'))

    def _check_https(self, *args):
        """
        Checks that https is working and that the provided certificate
        checks out
        """
        leap_assert(self._domain, "Cannot check HTTPS without a domain")
        logger.debug("Checking https for %r" % (self._domain))

        # We don't skip this check, since it's basic for the whole
        # system to work.
        # err --- but we can do it after a failure, to diagnose what went
        # wrong. Right now we're just adding connection overhead. -- kali

        verify = self.verify
        if verify:
            verify = self.verify.encode(sys.getfilesystemencoding())

        try:
            uri = "https://{0}".format(self._domain.encode('idna'))
            res = self._session.get(uri, verify=verify,
                                    timeout=REQUEST_TIMEOUT)
            res.raise_for_status()
        except requests.exceptions.SSLError as exc:
            self._err_msg = self.tr("Provider certificate could "
                                    "not be verified")
            raise
        except Exception as exc:
            # XXX careful!. The error might be also a SSL handshake
            # timeout error, in which case we should retry a couple of times
            # more, for cases where the ssl server gives high latencies.
            self._err_msg = self.tr("Provider does not support HTTPS")
            raise

    def _download_provider_info(self, *args):
        """
        Downloads the provider.json defition
        """
        leap_assert(self._domain,
                    "Cannot download provider info without a domain")
        logger.debug("Downloading provider info for %r" % (self._domain))

        # --------------------------------------------------------------
        # TODO factor out with the download routines in services.
        # Watch out! We're handling the verify paramenter differently here.

        headers = {}
        domain = self._domain.encode(sys.getfilesystemencoding())
        provider_json = os.path.join(util.get_path_prefix(),
                                     get_provider_path(domain))

        if domain in PinnedProviders.domains() and \
           not os.path.exists(provider_json):
            mkdir_p(os.path.join(os.path.dirname(provider_json),
                                 "keys", "ca"))
            cacert = os.path.join(os.path.dirname(provider_json),
                                  "keys", "ca", "cacert.pem")
            PinnedProviders.save_hardcoded(domain, provider_json, cacert)

        mtime = get_mtime(provider_json)

        if self._download_if_needed and mtime:
            headers['if-modified-since'] = mtime

        uri = "https://%s/%s" % (self._domain, "provider.json")
        verify = self.verify

        if mtime:  # the provider.json exists
            # So, we're getting it from the api.* and checking against
            # the provider ca.
            try:
                provider_config = ProviderConfig()
                provider_config.load(provider_json)
                uri = provider_config.get_api_uri() + '/provider.json'
                verify = provider_config.get_ca_cert_path()
            except MissingCACert:
                # no ca? then download from main domain again.
                pass

        if verify:
            verify = verify.encode(sys.getfilesystemencoding())
        logger.debug("Requesting for provider.json... "
                     "uri: {0}, verify: {1}, headers: {2}".format(
                         uri, verify, headers))
        res = self._session.get(uri.encode('idna'), verify=verify,
                                headers=headers, timeout=REQUEST_TIMEOUT)
        res.raise_for_status()
        logger.debug("Request status code: {0}".format(res.status_code))

        min_client_version = res.headers.get(self.MIN_CLIENT_VERSION, '0')

        # Not modified
        if res.status_code == 304:
            logger.debug("Provider definition has not been modified")
        # --------------------------------------------------------------
        # end refactor, more or less...
        # XXX