// Copyright 2016 The Go Authors.  All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// This binary contains an App Engine app for perfdata.golang.org
package main

import (
	"errors"
	"fmt"
	"log"
	"net/http"
	"os"
	"strings"
	"time"

	_ "github.com/go-sql-driver/mysql"
	"golang.org/x/net/context"
	"golang.org/x/perf/storage/app"
	"golang.org/x/perf/storage/db"
	"golang.org/x/perf/storage/fs/gcs"
	oauth2 "google.golang.org/api/oauth2/v2"
	"google.golang.org/appengine"
	aelog "google.golang.org/appengine/log"
	"google.golang.org/appengine/user"
)

// connectDB returns a DB initialized from the environment variables set in app.yaml. CLOUDSQL_CONNECTION_NAME, CLOUDSQL_USER, and CLOUDSQL_DATABASE must be set to point to the Cloud SQL instance. CLOUDSQL_PASSWORD can be set if needed.
func connectDB() (*db.DB, error) {
	var (
		connectionName = mustGetenv("CLOUDSQL_CONNECTION_NAME")
		user           = mustGetenv("CLOUDSQL_USER")
		password       = os.Getenv("CLOUDSQL_PASSWORD") // NOTE: password may be empty
		dbName         = mustGetenv("CLOUDSQL_DATABASE")
		socket         = os.Getenv("CLOUDSQL_SOCKET_PREFIX")
	)

	// /cloudsql is used on App Engine.
	if socket == "" {
		socket = "/cloudsql"
	}

	return db.OpenSQL("mysql", fmt.Sprintf("%s:%s@unix(%s/%s)/%s", user, password, socket, connectionName, dbName))
}

func mustGetenv(k string) string {
	v := os.Getenv(k)
	if v == "" {
		log.Panicf("%s environment variable not set.", k)
	}
	return v
}

func auth(w http.ResponseWriter, r *http.Request) (string, error) {
	ctx := appengine.NewContext(r)
	u, err := reqUser(ctx, r)
	if err != nil {
		return "", err
	}
	if u == "" {
		url, err := user.LoginURL(ctx, r.URL.String())
		if err != nil {
			return "", err
		}
		http.Redirect(w, r, url, http.StatusFound)
		return "", app.ErrResponseWritten
	}
	return u, nil
}

// reqUser gets the username from the request, trying AE user authentication, AE OAuth authentication, and Google OAuth authentication, in that order.
// If the request contains no authentication, "", nil is returned.
// If the request contains bogus authentication, an error is returned.
func reqUser(ctx context.Context, r *http.Request) (string, error) {
	u := user.Current(ctx)
	if u != nil {
		return u.Email, nil
	}
	if r.Header.Get("Authorization") == "" {
		return "", nil
	}
	u, err := user.CurrentOAuth(ctx, "https://www.googleapis.com/auth/userinfo.email")
	if err == nil {
		return u.Email, nil
	}
	return oauthServiceUser(ctx, r)
}

// oauthServiceUser authenticates the OAuth token from r's headers.
// This is necessary because user.CurrentOAuth does not work if the token is for a service account.
func oauthServiceUser(ctx context.Context, r *http.Request) (string, error) {
	tok := r.Header.Get("Authorization")
	if !strings.HasPrefix(tok, "Bearer ") {
		return "", errors.New("unknown Authorization header")
	}
	tok = tok[len("Bearer "):]

	service, err := oauth2.New(http.DefaultClient)
	if err != nil {
		return "", err
	}
	info, err := service.Tokeninfo().AccessToken(tok).Do()
	if err != nil {
		return "", err
	}

	if !info.VerifiedEmail || info.Email == "" {
		return "", errors.New("token does not contain verified e-mail address")
	}
	return info.Email, nil
}

// appHandler is the default handler, registered to serve "/".
// It creates a new App instance using the appengine Context and then
// dispatches the request to the App. The environment variable
// GCS_BUCKET must be set in app.yaml with the name of the bucket to
// write to. PERFDATA_VIEW_URL_BASE may be set to the URL that should
// be supplied in /upload responses.
func appHandler(w http.ResponseWriter, r *http.Request) {
	ctx := appengine.NewContext(r)
	// App Engine does not return a context with a deadline set,
	// even though the request does have a deadline. urlfetch uses
	// a 5s default timeout if the context does not have a
	// deadline, so explicitly set a deadline to match the App
	// Engine timeout.
	ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
	defer cancel()
	// GCS clients need to be constructed with an AppEngine
	// context, so we can't actually make the App until the
	// request comes in.
	// TODO(quentin): Figure out if there's a way to construct the
	// app and clients once, in init(), instead of on every request.
	db, err := connectDB()
	if err != nil {
		aelog.Errorf(ctx, "connectDB: %v", err)
		http.Error(w, err.Error(), 500)
		return
	}
	defer db.Close()

	fs, err := gcs.NewFS(ctx, mustGetenv("GCS_BUCKET"))
	if err != nil {
		aelog.Errorf(ctx, "gcs.NewFS: %v", err)
		http.Error(w, err.Error(), 500)
		return
	}
	mux := http.NewServeMux()
	app := &app.App{DB: db, FS: fs, Auth: auth, ViewURLBase: os.Getenv("PERFDATA_VIEW_URL_BASE")}
	app.RegisterOnMux(mux)
	mux.ServeHTTP(w, r)
}

func main() {
	http.HandleFunc("/", appHandler)
	appengine.Main()
}
