#!/usr/bin/env python
import argparse
from functools import wraps, partial
import json
import mimetypes
import os
import urllib.request, urllib.parse, urllib.error
import urllib.parse
import errno

import cherrypy
from cherrypy import wsgiserver
# from cherrypy.wsgiserver import ssl_pyopenssl
from cherrypy.wsgiserver.wsgiserver3 import WSGIPathInfoDispatcher
from jinja2.environment import Environment
from jinja2.loaders import FileSystemLoader
import yaml

from oic import rndstr
from oic.oic.provider import Provider, AuthorizationEndpoint, TokenEndpoint, \
    UserinfoEndpoint, RegistrationEndpoint, EndSessionEndpoint
from oic.utils.authn.authn_context import AuthnBroker
from oic.utils.authn.client import verify_client
from oic.utils.authz import AuthzHandling
from oic.utils.http_util import Redirect, SeeOther, Response, BadRequest, \
    get_or_post, get_post, NotFound
from oic.utils.keyio import keyjar_init
from oic.utils.sdb import SessionDB
from oic.utils.userinfo import UserInfo
from oic.utils.webfinger import OIC_ISSUER, WebFinger
from provider.authn import make_cls_from_name


def VerifierMiddleware(verifier):
    """Common wrapper for the authentication modules.
        * Parses the request before passing it on to the authentication module.
        * Sets 'pyoidc' cookie if authentication succeeds.
        * Redirects the user to complete the authentication.
        * Allows the user to retry authentication if it fails.
    :param verifier: authentication module
    """

    @wraps(verifier.verify)
    def wrapper(environ, start_response):
        data = get_post(environ)
        kwargs = dict(urllib.parse.parse_qsl(data))
        kwargs["state"] = json.loads(urllib.parse.unquote(kwargs["state"]))
        val, completed = verifier.verify(**kwargs)
        if not completed:
            return val(environ, start_response)
        if val:
            set_cookie, cookie_value = verifier.create_cookie(val, "auth")
            cookie_value += "; path=/"

            url = "{base_url}?{query_string}".format(
                base_url="/authorization",
                query_string=kwargs["state"]["query"])
            response = SeeOther(url, headers=[(set_cookie, cookie_value)])
            return response(environ, start_response)
        else:  # Unsuccessful authentication
            url = "{base_url}?{query_string}".format(
                base_url="/authorization",
                query_string=kwargs["state"]["query"])
            response = SeeOther(url)
            return response(environ, start_response)

    return wrapper


def pyoidcMiddleware(func):
    """Common wrapper for the underlying pyoidc library functions.
    Reads GET params and POST data before passing it on the library and
    converts the response from oic.utils.http_util to wsgi.
    :param func: underlying library function
    """

    def wrapper(environ, start_response):
        data = get_or_post(environ)
        cookies = environ.get("HTTP_COOKIE", "")
        resp = func(request=data, cookie=cookies)
        return resp(environ, start_response)

    return wrapper


def resp2flask(resp):
    """Convert an oic.utils.http_util instance to Flask."""
    if isinstance(resp, Redirect) or isinstance(resp, SeeOther):
        code = int(resp.status.split()[0])
        raise cherrypy.HTTPRedirect(resp.message, code)
    return resp.message, resp.status, resp.headers


def setup_authentication_methods(authn_config, template_env):
    """Add all authentication methods specified in the configuration."""
    routing = {}
    ac = AuthnBroker()
    for authn_method in authn_config:
        cls = make_cls_from_name(authn_method["class"])
        instance = cls(template_env=template_env, **authn_method["kwargs"])
        ac.add(authn_method["acr"], instance)
        routing[instance.url_endpoint] = VerifierMiddleware(instance)

    return ac, routing


def setup_endpoints(provider):
    """Setup the OpenID Connect Provider endpoints."""
    app_routing = {}
    endpoints = [
        AuthorizationEndpoint(
            pyoidcMiddleware(provider.authorization_endpoint)),
        TokenEndpoint(
            pyoidcMiddleware(provider.token_endpoint)),
        UserinfoEndpoint(
            pyoidcMiddleware(provider.userinfo_endpoint)),
        RegistrationEndpoint(
            pyoidcMiddleware(provider.registration_endpoint)),
        EndSessionEndpoint(
            pyoidcMiddleware(provider.endsession_endpoint))
    ]

    for ep in endpoints:
        app_routing["/{}".format(ep.etype)] = ep

    return app_routing


def _webfinger(provider, request, **kwargs):
    """Handle webfinger requests."""
    params = urllib.parse.parse_qs(request)
    if params["rel"][0] == OIC_ISSUER:
        wf = WebFinger()
        return Response(wf.response(params["resource"][0], provider.baseurl),
                        headers=[("Content-Type", "application/jrd+json")])
    else:
        return BadRequest("Incorrect webfinger.")


def make_static_handler(static_dir):
    def static(environ, start_response):
        path = environ['PATH_INFO']
        full_path = os.path.join(
            static_dir, os.path.normpath(path).lstrip("/"))

        if os.path.exists(full_path):
            with open(full_path, 'r') as f:
                content = f.read()

            content_type, encoding = mimetypes.guess_type(full_path)
            headers = [('Content-Type', content_type)]
            start_response("200 OK", headers)
            return [content]
        else:
            response = NotFound(
                "File '{}' not found.".format(environ['PATH_INFO']))
            return response(environ, start_response)

    return static


def main():
    parser = argparse.ArgumentParser(description='Example OIDC Provider.')
    parser.add_argument("-p", "--port", default=80, type=int)
    parser.add_argument("-b", "--base", default="https://localhost", type=str)
    parser.add_argument("-d", "--debug", action="store_true")
    parser.add_argument("settings")
    args = parser.parse_args()

    # Load configuration
    with open(args.settings, "r") as f:
        settings = yaml.load(f)

    issuer = args.base.rstrip("/")

    template_dirs = settings["server"].get("template_dirs", "templates")
    jinja_env = Environment(loader=FileSystemLoader(template_dirs))
    authn_broker, auth_routing = setup_authentication_methods(
        settings["authn"], jinja_env)

    # Setup userinfo
    userinfo_conf = settings["userinfo"]
    cls = make_cls_from_name(userinfo_conf["class"])
    i = cls(**userinfo_conf["kwargs"])
    userinfo = UserInfo(i)

    client_db = {}
    provider = Provider(issuer, SessionDB(issuer), client_db, authn_broker,
                        userinfo, AuthzHandling(), verify_client, None)
    provider.baseurl = issuer
    provider.symkey = rndstr(16)

    # Setup keys
    path = os.path.join(os.path.dirname(__file__), "static")
    try:
        os.makedirs(path)
    except OSError as e:
        if e.errno != errno.EEXIST:
            raise e
        pass
    jwks = keyjar_init(provider, settings["provider"]["keys"])
    name = "jwks.json"
    with open(os.path.join(path, name), "w") as f:
        f.write(json.dumps(jwks))

    provider.jwks_uri.append(
        "{}/static/{}".format(provider.baseurl, name))

    # Mount the WSGI callable object (app) on the root directory
    app_routing = setup_endpoints(provider)
    app_routing["/.well-known/openid-configuration"] = pyoidcMiddleware(
        provider.providerinfo_endpoint)
    app_routing["/.well-known/webfinger"] = pyoidcMiddleware(
        partial(_webfinger, provider))
    routing = {**auth_routing, **app_routing}
    routing["/static"] = make_static_handler(path)
    dispatcher = WSGIPathInfoDispatcher(routing)
    server = wsgiserver.CherryPyWSGIServer(('0.0.0.0', args.port), dispatcher)

    # Setup SSL
    # if provider.baseurl.startswith("https://"):
    #     server.ssl_adapter = ssl_pyopenssl.pyOpenSSLAdapter(
    #         settings["server"]["cert"], settings["server"]["key"],
    #         settings["server"]["cert_chain"])

    # Start the CherryPy WSGI web server
    try:
        print("Server started: {}".format(issuer))
        server.start()
    except KeyboardInterrupt:
        server.stop()


if __name__ == "__main__":
    main()
