package user

import (
	"context"
	"fmt"
	"net/http"
	"net/url"
	"strings"

	"golang.org/x/oauth2"
	"gorm.io/gorm"

	"github.com/google/go-github/v41/github"
	"github.com/porter-dev/porter/api/server/authn"
	"github.com/porter-dev/porter/api/server/handlers"
	"github.com/porter-dev/porter/api/server/shared"
	"github.com/porter-dev/porter/api/server/shared/apierrors"
	"github.com/porter-dev/porter/api/server/shared/config"
	"github.com/porter-dev/porter/internal/analytics"
	"github.com/porter-dev/porter/internal/models"
)

type UserOAuthGithubCallbackHandler struct {
	handlers.PorterHandlerReadWriter
}

func NewUserOAuthGithubCallbackHandler(
	config *config.Config,
	decoderValidator shared.RequestDecoderValidator,
	writer shared.ResultWriter,
) *UserOAuthGithubCallbackHandler {
	return &UserOAuthGithubCallbackHandler{
		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
	}
}

func (p *UserOAuthGithubCallbackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	session, err := p.Config().Store.Get(r, p.Config().ServerConf.CookieName)
	if err != nil {
		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
		return
	}

	if _, ok := session.Values["state"]; !ok {
		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
		return
	}

	if r.URL.Query().Get("state") != session.Values["state"] {
		p.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
		return
	}

	token, err := p.Config().GithubConf.Exchange(oauth2.NoContext, r.URL.Query().Get("code"))
	if err != nil {
		p.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
		return
	}

	if !token.Valid() {
		p.HandleAPIError(w, r, apierrors.NewErrForbidden(fmt.Errorf("invalid token")))
		return
	}

	// otherwise, create the user if not exists
	user, err := upsertUserFromToken(p.Config(), token)

	if err != nil && strings.Contains(err.Error(), "already registered") {
		http.Redirect(w, r, "/login?error="+url.QueryEscape(err.Error()), 302)
		return
	} else if err != nil {
		http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
		return
	}

	p.Config().AnalyticsClient.Identify(analytics.CreateSegmentIdentifyUser(user))

	// save the user as authenticated in the session
	redirect, err := authn.SaveUserAuthenticated(w, r, p.Config(), user)
	if err != nil {
		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
		return
	}

	// non-fatal send email verification
	if !user.EmailVerified {
		err = startEmailVerification(p.Config(), w, r, user)

		if err != nil {
			p.HandleAPIErrorNoWrite(w, r, apierrors.NewErrInternal(err))
		}
	}

	if redirect != "" {
		http.Redirect(w, r, redirect, http.StatusFound)
		return
	}

	http.Redirect(w, r, "/dashboard", 302)
}

func upsertUserFromToken(config *config.Config, tok *oauth2.Token) (*models.User, error) {
	// determine if the user already exists
	client := github.NewClient(config.GithubConf.Client(oauth2.NoContext, tok))

	githubUser, _, err := client.Users.Get(context.Background(), "")
	if err != nil {
		return nil, err
	}

	user, err := config.Repo.User().ReadUserByGithubUserID(*githubUser.ID)

	// if the user does not exist, create new user
	if err != nil && err == gorm.ErrRecordNotFound {
		emails, _, err := client.Users.ListEmails(context.Background(), &github.ListOptions{})
		if err != nil {
			return nil, err
		}

		primary := ""
		verified := false

		// get the primary email
		for _, email := range emails {
			if email.GetPrimary() {
				primary = email.GetEmail()
				verified = email.GetVerified()
				break
			}
		}

		if primary == "" {
			return nil, fmt.Errorf("github user must have an email")
		}

		if err := checkUserRestrictions(config.ServerConf, primary); err != nil {
			return nil, err
		}

		// check if a user with that email address already exists
		_, err = config.Repo.User().ReadUserByEmail(primary)

		if err == gorm.ErrRecordNotFound {
			user = &models.User{
				Email:         primary,
				EmailVerified: !config.Metadata.Email || verified,
				GithubUserID:  githubUser.GetID(),
			}

			user, err = config.Repo.User().CreateUser(user)

			if err != nil {
				return nil, err
			}

			err = addUserToDefaultProject(config, user)

			if err != nil {
				return nil, err
			}
		} else if err == nil {
			return nil, fmt.Errorf("email already registered")
		} else if err != nil {
			return nil, err
		}
	} else if err != nil {
		return nil, fmt.Errorf("unexpected error occurred:%s", err.Error())
	}

	return user, nil
}
