# Copyright 2014
# The Cloudscaling Group, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import base64
import json
import time
import uuid

from keystoneclient import client as keystone_client
from keystoneclient import exceptions
from oslo_config import cfg
from oslo_log import log as logging
import webob

from gceapi.i18n import _
from gceapi import wsgi_ext as openstack_wsgi

CONF = cfg.CONF
LOG = logging.getLogger(__name__)


INTERNAL_GCUTIL_PROJECTS = ["debian-cloud", "centos-cloud", "suse-cloud",
                            "rhel-cloud", "windows-cloud", "google",
                            "coreos-cloud", "opensuse-cloud"]


class OAuthFault(openstack_wsgi.Fault):
    """Fault compliant with RFC

    To prevent extra info added by openstack.wsgi.Fault class
    to response which is not compliant RFC6749.
    """
    @webob.dec.wsgify(RequestClass=openstack_wsgi.Request)
    def __call__(self, req):
        return self.wrapped_exc


class Controller(object):
    """Simple OAuth2.0 Controller

    If you need other apps to work with GCE API you should add it here
    in VALID_CLIENTS.
    Based on https://developers.google.com/accounts/docs/OAuth2InstalledApp
    and on RFC 6749(paragraph 4.1).
    """

    AUTH_TIMEOUT = 300
    VALID_CLIENTS = {
        "32555940559.apps.googleusercontent.com": "ZmssLNjJy2998hD4CTg2ejr2",
        "1025389682001.apps.googleusercontent.com": "xslsVXhA7C8aOfSfb6edB6p6",
    }

    INTERNAL_REDIRECT_URI = "urn:ietf:wg:oauth:2.0:oob"
    AUTH_PAGE_TEMPLATE =\
        "<!DOCTYPE html>"\
        "<html xmlns=\"http://www.w3.org/1999/xhtml\"><body>"\
        "Enter Openstack username and password to access GCE API<br/>"\
        "<br/>"\
        "<form action=\"approval\" name=\"approval\" method=\"post\">"\
        "<input type=\"hidden\" name=\"redirect_uri\" value=\""\
        + "{redirect_uri}\"/>"\
        "<input type=\"hidden\" name=\"code\" value=\"{code}\"/>"\
        "<input type=\"text\" name=\"username\" value=\"\"/><br/>"\
        "<input type=\"password\" name=\"password\" value=\"\"/><br/>"\
        "<input type=\"submit\" value=\"Login\"/>"\
        "</form>"\
        "</body></html>"

    class Client(object):
        auth_start_time = 0
        auth_token = None
        expires_in = 1

    # NOTE(apavlov): there is no cleaning of the dictionary
    _clients = {}

    def _check_redirect_uri(self, uri):
        if uri is None:
            msg = _("redirect_uri should be present")
            raise webob.exc.HTTPBadRequest(explanation=msg)
        if "localhost" not in uri and uri != self.INTERNAL_REDIRECT_URI:
            msg = _("redirect_uri has invalid format."
                    "it must confirms installed application uri of GCE")
            json_body = {"error": "invalid_request",
                         "error_description": msg}
            raise OAuthFault(webob.exc.HTTPBadRequest(json_body=json_body))

    def auth(self, req):
        """OAuth protocol authorization endpoint handler

        Returns login authorization webpage invoked for example by gcutil auth.
        """
        client_id = req.GET.get("client_id")
        if client_id is None or client_id not in self.VALID_CLIENTS:
            json_body = {"error": "unauthorized_client"}
            raise OAuthFault(webob.exc.HTTPBadRequest(json_body=json_body))

        if req.GET.get("response_type") != "code":
            json_body = {"error": "unsupported_response_type"}
            raise OAuthFault(webob.exc.HTTPBadRequest(json_body=json_body))
        self._check_redirect_uri(req.GET.get("redirect_uri"))

        code = base64.urlsafe_b64encode(uuid.uuid4().bytes).replace('=', '')
        self._clients[code] = self.Client()
        self._clients[code].auth_start_time = time.time()

        html_page = self.AUTH_PAGE_TEMPLATE.format(
            redirect_uri=req.GET.get("redirect_uri"),
            code=code)
        return html_page

    def approval(self, req):
        """OAuth protocol authorization endpoint handler second part

        Returns webpage with verification code or redirects to provided
        redirect_uri specified in auth request.
        """
        code = req.POST.get("code")
        if code is None:
            json_body = {"error": "invalid_request"}
            raise OAuthFault(webob.exc.HTTPBadRequest(json_body=json_body))

        client = self._clients.get(code)
        if client is None:
            json_body = {"error": "invalid_client"}
            raise OAuthFault(webob.exc.HTTPBadRequest(json_body=json_body))

        if time.time() - client.auth_start_time > self.AUTH_TIMEOUT:
            raise webob.exc.HTTPRequestTimeout()

        redirect_uri = req.POST.get("redirect_uri")
        self._check_redirect_uri(redirect_uri)

        username = req.POST.get("username")
        password = req.POST.get("password")

        try:
            keystone = keystone_client.Client(
                username=username,
                password=password,
                auth_url=CONF.keystone_url)
            if keystone.auth_ref is None:
                # Ver2 doesn't create session and performs
                # authentication automatically, but Ver3 does create session
                # if it's not provided and doesn't perform authentication.
                # TODO(alexy-mr): use sessions
                keystone.authenticate()
            client.auth_token = keystone.auth_token
            s = keystone.auth_ref.issued
            e = keystone.auth_ref.expires
            client.expires_in = (e - s).seconds
        except Exception as ex:
            return webob.exc.HTTPUnauthorized(ex)

        if redirect_uri == self.INTERNAL_REDIRECT_URI:
            return "<html><body>Verification code is: "\
                + code + "</body></html>"

        uri = redirect_uri + "?code=" + code
        raise webob.exc.HTTPFound(location=uri)

    def token(self, req):
        """OAuth protocol authorization endpoint handler second part

        Returns json with tokens(access_token and optionally refresh_token).
        """
        client_id = req.POST.get("client_id")
        if client_id is None or client_id not in self.VALID_CLIENTS:
            json_body = {"error": "unauthorized_client"}
            raise OAuthFault(webob.exc.HTTPBadRequest(json_body=json_body))
        valid_secret = self.VALID_CLIENTS[client_id]
        client_secret = req.POST.get("client_secret")
        if client_secret is None or client_secret != valid_secret:
            json_body = {"error": "unauthorized_client"}
            raise OAuthFault(webob.exc.HTTPBadRequest(json_body=json_body))

        if req.POST.get("grant_type") != "authorization_code":
            json_body = {"error": "unsupported_grant_type"}
            raise OAuthFault(webob.exc.HTTPBadRequest(json_body=json_body))

        code = req.POST.get("code")
        client = self._clients.get(code)
        if client is None:
            json_body = {"error": "invalid_client"}
            raise OAuthFault(webob.exc.HTTPBadRequest(json_body=json_body))

        result = {"access_token": client.auth_token,
                  "expires_in": client.expires_in,
                  "token_type": "Bearer"}
        return json.dumps(result)


class AuthProtocol(object):
    """Filter for translating oauth token to keystone token."""
    def __init__(self, app):
        self.app = app
        self.auth_url = CONF.keystone_url

    def __call__(self, env, start_response):
        auth_token = env.get("HTTP_AUTHORIZATION")
        if auth_token is None:
            return self._reject_request(start_response)

        project = env["PATH_INFO"].split("/")[1]
        try:
            keystone = keystone_client.Client(
                token=auth_token.split()[1],
                tenant_name=project,
                force_new_token=True,
                auth_url=self.auth_url)
            if keystone.auth_ref is None:
                # Ver2 doesn't create session and performs
                # authentication automatically, but Ver3 does create session
                # if it's not provided and doesn't perform authentication.
                # TODO(alexey-mr): use sessions
                keystone.authenticate()
            scoped_token = keystone.auth_token
            env["HTTP_X_AUTH_TOKEN"] = scoped_token
            return self.app(env, start_response)
        except exceptions.Unauthorized:
            if project in INTERNAL_GCUTIL_PROJECTS:
                # NOTE(apavlov): return empty if no such projects(by gcutil)
                headers = [('Content-type', 'application/json;charset=UTF-8')]
                start_response('200 Ok', headers)
                return ["{}"]

            return self._reject_request(start_response)

    def _reject_request(self, start_response):
        headers = [('Content-type', 'application/json;charset=UTF-8')]
        start_response('401 Unauthorized', headers)
        json_body = {"error": "access_denied"}
        return [json.dumps(json_body)]


def filter_factory(global_conf, **local_conf):
    def auth_filter(app):
        return AuthProtocol(app)
    return auth_filter


def create_resource():
    return openstack_wsgi.Resource(Controller())