//
//  tank
//
//  Import and Playlist Daemon for autoradio project
//
//
//  Copyright (C) 2017-2019 Christian Pointner <equinox@helsinki.at>
//
//  This file is part of tank.
//
//  tank 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
//  any later version.
//
//  tank 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 tank. If not, see <http://www.gnu.org/licenses/>.
//

package auth

import (
	"context"
	"net/http"

	oidc "github.com/coreos/go-oidc"
	"golang.org/x/oauth2"
)

type OIDCSession struct {
	State string
	Nonce string
}

func NewOIDCSession() (s *OIDCSession, err error) {
	s = &OIDCSession{}
	if s.State, err = generateRandomString(32); err != nil {
		return
	}
	if s.Nonce, err = generateRandomString(32); err != nil {
		return
	}
	return
}

// TODO: check concurrent usage of provider/verifier etc...

type OIDCBackend struct {
	provider     *oidc.Provider
	verifier     *oidc.IDTokenVerifier
	oauth2Config oauth2.Config
}

func NewOIDCBackend(cfg *OIDCConfig) (b *OIDCBackend, err error) {
	// TODO: make ctx a parameter?
	ctx := context.Background()
	b = &OIDCBackend{}
	if b.provider, err = oidc.NewProvider(ctx, cfg.IssuerURL); err != nil {
		return
	}

	oidcConfig := &oidc.Config{
		ClientID: cfg.ClientID,
	}
	b.verifier = b.provider.Verifier(oidcConfig)

	b.oauth2Config = oauth2.Config{
		ClientID:     cfg.ClientID,
		ClientSecret: cfg.ClientSecret,
		Endpoint:     b.provider.Endpoint(),
		RedirectURL:  cfg.CallbackURL,
		Scopes:       []string{oidc.ScopeOpenID, "username", "aura_shows"},
	}

	return
}

func (b *OIDCBackend) LoginHandler() http.Handler {
	return &oidcLoginHandler{backend: b}
}

func (b *OIDCBackend) CallbackHandler() http.Handler {
	return &oidcCallbackHandler{backend: b}
}

type oidcLoginHandler struct {
	backend *OIDCBackend
}

// TODO: error handling?
func (h *oidcLoginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// TODO: check if we alreay have a session coockie!
	s, err := NewOIDCSession()
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}

	// TODO: add session to session-manager and create session coockie

	http.Redirect(w, r, h.backend.oauth2Config.AuthCodeURL(s.State, oidc.Nonce(s.Nonce)), http.StatusFound)
}

type oidcCallbackHandler struct {
	backend *OIDCBackend
}

func (h *oidcCallbackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// TODO: check if we have a session coockie!

	// TODO: get OIDCSession from session-manager, based on session coockie
	os := &OIDCSession{}

	if r.URL.Query().Get("state") != os.State {
		http.Error(w, "OIDC: state did not match", http.StatusBadRequest)
		return
	}

	oauth2Token, err := h.backend.oauth2Config.Exchange(r.Context(), r.URL.Query().Get("code"))
	if err != nil {
		http.Error(w, "OIDC: failed to exchange token: "+err.Error(), http.StatusInternalServerError)
		return
	}

	rawIDToken, ok := oauth2Token.Extra("id_token").(string)
	if !ok {
		http.Error(w, "OIDC: no id_token field in oauth2 token.", http.StatusInternalServerError)
		return
	}
	// Verify the ID Token signature and nonce.
	idToken, err := h.backend.verifier.Verify(r.Context(), rawIDToken)
	if err != nil {
		http.Error(w, "OIDC: failed to verify ID Token: "+err.Error(), http.StatusInternalServerError)
		return
	}
	if idToken.Nonce != os.Nonce {
		http.Error(w, "OIDC: invalid ID token nonce", http.StatusInternalServerError)
		return
	}

	// TODO: parse userInfo, populeate new session and update it inside the session-manager

	// userInfo, err := h.backend.provider.UserInfo(r.Context(), oauth2.StaticTokenSource(oauth2Token))
	// if err != nil {
	// 	http.Error(w, "OIDC: failed to get userinfo: "+err.Error(), http.StatusInternalServerError)
	// 	return
	// }
}