// Package crowdauth provides middleware for Crowd SSO logins
//
// Goals:
//  1) drop-in authentication against Crowd SSO
//  2) make it easy to use Crowd SSO as part of your own auth flow
package crowdauth // import "go.jona.me/crowd/crowdauth"

import (
	"errors"
	"go.jona.me/crowd"
	"html/template"
	"log"
	"net/http"
	"time"
)

type SSO struct {
	CrowdApp            *crowd.Crowd
	LoginPage           AuthLoginPage
	LoginTemplate       *template.Template
	ClientAddressFinder ClientAddressFinder
	CookieConfig        crowd.CookieConfig
}

// The AuthLoginPage type extends the normal http.HandlerFunc type
// with a boolean return to indicate login success or failure.
type AuthLoginPage func(http.ResponseWriter, *http.Request, *SSO) bool

// ClientAddressFinder type represents a function that returns the
// end-user's IP address, allowing you to handle cases where the address
// is masked by proxy servers etc by checking headers or whatever to find
// the user's address
type ClientAddressFinder func(*http.Request) (string, error)

var authErr string = "unauthorized, login required"

func DefaultClientAddressFinder(r *http.Request) (addr string, err error) {
	return r.RemoteAddr, nil
}

// New creates a new instance of SSO
func New(user string, password string, crowdURL string) (s *SSO, err error) {
	s = &SSO{}
	s.LoginPage = loginPage
	s.ClientAddressFinder = DefaultClientAddressFinder
	s.LoginTemplate = template.Must(template.New("authPage").Parse(defLoginPage))

	cwd, err := crowd.New(user, password, crowdURL)
	if err != nil {
		return s, err
	}
	s.CrowdApp = &cwd
	s.CookieConfig, err = s.CrowdApp.GetCookieConfig()
	if err != nil {
		return s, err
	}

	return s, nil
}

// Handler provides HTTP middleware using http.Handler chaining
// that requires user authentication via Atlassian Crowd SSO.
func (s *SSO) Handler(h http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if s.loginHandler(w, r) == false {
			return
		}
		h.ServeHTTP(w, r)
	})

}

func (s *SSO) loginHandler(w http.ResponseWriter, r *http.Request) bool {
	ck, err := r.Cookie(s.CookieConfig.Name)

	if err == http.ErrNoCookie {
		// no cookie so show login page if GET
		// if POST check if login and handle
		// if fail, show login page with message
		if r.Method == "GET" {
			s.LoginPage(w, r, s)
		} else if r.Method == "POST" {
			authOK := s.LoginPage(w, r, s)
			if authOK == true {
				// Redirect for fresh pass through auth etc on success
				http.Redirect(w, r, r.RequestURI, http.StatusTemporaryRedirect)
				return false
			} else {
				log.Printf("crowdauth: authentication failed\n")
			}
		} else {
			http.Error(w, authErr, http.StatusUnauthorized)
		}
		return false
	} else {
		// validate cookie or show login page
		host, err := s.ClientAddressFinder(r)
		if err != nil {
			log.Printf("crowdauth: could not get remote addr: %s\n", err)
			return false
		}

		_, err = s.CrowdApp.ValidateSession(ck.Value, host)
		if err != nil {
			log.Printf("crowdauth: could not validate cookie, deleting because: %s\n", err)
			s.EndSession(w, r)
			s.LoginPage(w, r, s)
			return false
		}

		// valid cookie so fallthrough
	}
	return true
}

func (s *SSO) Login(user string, pass string, addr string) (cs crowd.Session, err error) {
	cs, err = s.CrowdApp.NewSession(user, pass, addr)
	return cs, err
}

func (s *SSO) Logout(w http.ResponseWriter, r *http.Request, newURL string) {
	s.EndSession(w, r)
	http.Redirect(w, r, newURL, http.StatusTemporaryRedirect)
}

// StartSession sets a Crowd session cookie.
func (s *SSO) StartSession(w http.ResponseWriter, cs crowd.Session) {
	ck := http.Cookie{
		Name:    s.CookieConfig.Name,
		Domain:  s.CookieConfig.Domain,
		Secure:  s.CookieConfig.Secure,
		Value:   cs.Token,
		Expires: cs.Expires,
	}
	http.SetCookie(w, &ck)
}

// EndSession invalidates the current Crowd session and cookie
func (s *SSO) EndSession(w http.ResponseWriter, r *http.Request) {
	currentCookie, _ := r.Cookie(s.CookieConfig.Name)
	newCookie := &http.Cookie{
		Name:    s.CookieConfig.Name,
		Domain:  s.CookieConfig.Domain,
		Secure:  s.CookieConfig.Secure,
		MaxAge:  -1,
		Expires: time.Unix(1, 0),
		Value:   "LOGGED-OUT",
	}
	s.CrowdApp.InvalidateSession(currentCookie.Value)

	log.Printf("Got cookie to remove: %+v\n", currentCookie)
	log.Printf("Removal cookie: %+v\n", newCookie)
	http.SetCookie(w, newCookie)
}

// Get User information for the current session (by cookie)
func (s *SSO) GetUser(r *http.Request) (u crowd.User, err error) {
	currentCookie, err := r.Cookie(s.CookieConfig.Name)
	if err == http.ErrNoCookie {
		return u, errors.New("no session cookie")
	}

	userSession, err := s.CrowdApp.GetSession(currentCookie.Value)
	if err != nil {
		return u, errors.New("session not valid")
	}

	return userSession.User, nil
}

func loginPage(w http.ResponseWriter, r *http.Request, s *SSO) bool {
	if r.Method == "GET" { // show login page and bail
		showLoginPage(w, s)
		return false
	} else if r.Method == "POST" {
		user := r.FormValue("username")
		pass := r.FormValue("password")
		host, err := s.ClientAddressFinder(r)
		if err != nil {
			log.Printf("crowdauth: could not get remote addr: %s\n", err)
			showLoginPage(w, s)
			return false
		}

		sess, err := s.Login(user, pass, host)
		if err != nil {
			log.Printf("crowdauth: login/new session failed: %s\n", err)
			showLoginPage(w, s)
			return false
		}

		s.StartSession(w, sess)
	} else {
		return false
	}

	return true
}

func showLoginPage(w http.ResponseWriter, s *SSO) {
	err := s.LoginTemplate.ExecuteTemplate(w, "authPage", nil)
	if err != nil {
		log.Printf("crowdauth: could not exec template: %s\n", err)
	}
}