package main

import (
	"crypto/x509"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"regexp"
	"strconv"

	"github.com/govau/cf-common/env"
	"github.com/labstack/echo/v4"
	uuid "github.com/satori/go.uuid"
	log "github.com/sirupsen/logrus"

	"github.com/cloudfoundry/stratos/src/jetstream/api"
	"github.com/cloudfoundry/stratos/src/jetstream/api/config"
	"github.com/cloudfoundry/stratos/src/jetstream/crypto"
	"github.com/cloudfoundry/stratos/src/jetstream/repository/console_config"
	"github.com/cloudfoundry/stratos/src/jetstream/repository/localusers"
)

const (
	setupRequestRegex      = "^/pp/v1/setup/save$"
	setupCheckRequestRegex = "^/pp/v1/setup/check$"
	versionRequestRegex    = "^/pp/v1/version$"
	pingRequestRegex       = "^/pp/v1/ping$"
	backendRequestRegex    = "^/pp/v1/"
	apiRequestRegex        = "^/api/v1/"
	systemGroupName        = "env"
)

func parseConsoleConfigFromForm(c echo.Context) (*api.ConsoleConfig, error) {
	consoleConfig := new(api.ConsoleConfig)

	// Local admin user configuration?
	password := c.FormValue("local_admin_password")
	if len(password) > 0 {
		consoleConfig.LocalUserPassword = password
		consoleConfig.AuthEndpointType = "local"
		return consoleConfig, nil
	}

	url, err := url.Parse(c.FormValue("uaa_endpoint"))
	if err != nil {
		return nil, echo.NewHTTPError(http.StatusBadRequest, "Invalid UAA Endpoint value")
	}

	consoleConfig.UAAEndpoint = url
	// Default auth endpoint to the same value as UAA Endpoint when setup via the UI setup (for now)
	consoleConfig.AuthorizationEndpoint = url
	consoleConfig.ConsoleClient = c.FormValue("console_client")
	consoleConfig.ConsoleClientSecret = c.FormValue("console_client_secret")

	skipSSLValidation, err := strconv.ParseBool(c.FormValue("skip_ssl_validation"))
	if err != nil {
		return nil, echo.NewHTTPError(http.StatusBadRequest, "Invalid Skip SSL Validation value")
	}
	consoleConfig.SkipSSLValidation = skipSSLValidation

	ssoLogin, err := strconv.ParseBool(c.FormValue("use_sso"))
	if err != nil {
		return nil, echo.NewHTTPError(http.StatusBadRequest, "Invalid Use SSO value")
	}
	consoleConfig.UseSSO = ssoLogin
	consoleConfig.ConsoleAdminScope = c.FormValue("console_admin_scope")

	return consoleConfig, nil
}

// Check the initial parameter set and fetch the list of available scopes
// This does not persist the configuration to the database at this stage
func (p *portalProxy) setupGetAvailableScopes(c echo.Context) error {

	// Check if already set up
	if p.GetConfig().ConsoleConfig.IsSetupComplete() {
		return c.NoContent(http.StatusServiceUnavailable)
	}

	consoleConfig, err := parseConsoleConfigFromForm(c)
	if err != nil {
		return err
	}

	username := c.FormValue("username")
	password := c.FormValue("password")

	// Authenticate with UAA
	authEndpoint := fmt.Sprintf("%s/oauth/token", consoleConfig.UAAEndpoint)
	uaaRes, err := p.getUAATokenWithCreds(consoleConfig.SkipSSLValidation, username, password, consoleConfig.ConsoleClient, consoleConfig.ConsoleClientSecret, authEndpoint)
	if err != nil {
		errInfo, ok := err.(api.ErrHTTPRequest)
		if ok {
			if errInfo.Status == 0 {
				var certError *x509.CertificateInvalidError
				if errors.As(err, certError) {
					return api.NewHTTPShadowError(
						http.StatusBadRequest,
						"Could not connect to the UAA - Certificate error - check Skip SSL validation setting",
						"Could not connect to the UAA - Certificate error - check Skip SSL validation setting: %+v", err)
				}
				return api.NewHTTPShadowError(
					http.StatusBadRequest,
					"Could not connect to the UAA - check UAA Endpoint URL",
					"Could not connect to the UAA - check UAA Endpoint URL: %+v", err)
			}
		}
		return api.NewHTTPShadowError(
			http.StatusBadRequest,
			"Failed to authenticate with UAA - check Client ID, Secret and credentials",
			"Failed to authenticate with UAA due to %s", err)
	}

	userTokenInfo, err := p.GetUserTokenInfo(uaaRes.AccessToken)
	if err != nil {
		return api.NewHTTPShadowError(
			http.StatusBadRequest,
			"Failed to authenticate with UAA - check Client ID, Secret and credentials",
			"Failed to authenticate with UAA due to %s", err)
	}

	c.JSON(http.StatusOK, userTokenInfo)
	return nil
}

func saveConsoleConfig(consoleRepo console_config.Repository, consoleConfig *api.ConsoleConfig) error {
	if api.AuthEndpointTypes[consoleConfig.AuthEndpointType] == api.Local {
		return saveLocalUserConsoleConfig(consoleRepo, consoleConfig)
	}

	return saveUAAConsoleConfig(consoleRepo, consoleConfig)
}

func saveLocalUserConsoleConfig(consoleRepo console_config.Repository, consoleConfig *api.ConsoleConfig) error {

	log.Debug("saveLocalUserConsoleConfig")

	if err := consoleRepo.SetValue(systemGroupName, "AUTH_ENDPOINT_TYPE", "local"); err != nil {
		return err
	}

	if err := consoleRepo.SetValue(systemGroupName, "CONSOLE_ADMIN_SCOPE", "stratos.admin"); err != nil {
		return err
	}

	if err := consoleRepo.SetValue(systemGroupName, "LOCAL_USER", "admin"); err != nil {
		return err
	}

	if err := consoleRepo.SetValue(systemGroupName, "LOCAL_USER_SCOPE", "stratos.admin"); err != nil {
		return err
	}

	// Do not save the raw password - we will create the account during setup, so we don't need it beyond that
	// We need to store a value so that console believes everything is setup - but we have created the account
	// already, so we don't need to save the actual password
	if err := consoleRepo.SetValue(systemGroupName, "LOCAL_USER_PASSWORD", "--"); err != nil {
		return err
	}

	return nil
}

func saveUAAConsoleConfig(consoleRepo console_config.Repository, consoleConfig *api.ConsoleConfig) error {
	log.Debugf("Saving ConsoleConfig: %+v", consoleConfig)

	if err := consoleRepo.SetValue(systemGroupName, "UAA_ENDPOINT", consoleConfig.UAAEndpoint.String()); err != nil {
		return err
	}

	if err := consoleRepo.SetValue(systemGroupName, "AUTHORIZATION_ENDPOINT", consoleConfig.AuthorizationEndpoint.String()); err != nil {
		return err
	}

	if err := consoleRepo.SetValue(systemGroupName, "CONSOLE_CLIENT", consoleConfig.ConsoleClient); err != nil {
		return err
	}

	if err := consoleRepo.SetValue(systemGroupName, "CONSOLE_CLIENT_SECRET", consoleConfig.ConsoleClientSecret); err != nil {
		return err
	}

	if err := consoleRepo.SetValue(systemGroupName, "SKIP_SSL_VALIDATION", strconv.FormatBool(consoleConfig.SkipSSLValidation)); err != nil {
		return err
	}

	if err := consoleRepo.SetValue(systemGroupName, "SSO_LOGIN", strconv.FormatBool(consoleConfig.UseSSO)); err != nil {
		return err
	}

	if err := consoleRepo.SetValue(systemGroupName, "CONSOLE_ADMIN_SCOPE", consoleConfig.ConsoleAdminScope); err != nil {
		return err
	}

	return nil
}

// Save the console setup data to the database
func (p *portalProxy) setupSaveConfig(c echo.Context) error {

	log.Debug("setupSaveConfig")

	consoleRepo, err := console_config.NewPostgresConsoleConfigRepository(p.DatabaseConnectionPool)
	if err != nil {
		return echo.NewHTTPError(http.StatusForbidden, "Failed to connect to Database!")
	}

	// Check if already set up
	if p.GetConfig().ConsoleConfig.IsSetupComplete() {
		return c.NoContent(http.StatusServiceUnavailable)
	}

	consoleConfig, err := parseConsoleConfigFromForm(c)
	if err != nil {
		return err
	}

	err = saveConsoleConfig(consoleRepo, consoleConfig)
	if err != nil {
		return api.NewHTTPShadowError(
			http.StatusInternalServerError,
			"Failed to store Console configuration data",
			"Console configuration data storage failed due to %s", err)
	}

	// If setting up with a local admin user, then log the user in
	if api.AuthEndpointTypes[consoleConfig.AuthEndpointType] == api.Local {
		consoleConfig.LocalUser = "admin"
		if consoleConfig.IsSetupComplete() {
			p.GetConfig().ConsoleConfig.AuthEndpointType = "local"
			p.InitStratosAuthService(api.Local)
			c.Request().Form.Add("username", "admin")
			c.Request().Form.Add("password", consoleConfig.LocalUserPassword)
			c.Request().RequestURI = "/pp/v1/login"
			setupInitialiseLocalUsersConfiguration(consoleConfig, p)
			return p.consoleLogin(c)
		}
	}

	c.NoContent(http.StatusOK)
	return nil
}

func (p *portalProxy) initialiseConsoleConfig(envLookup *env.VarSet) (*api.ConsoleConfig, error) {
	log.Debug("initialiseConsoleConfig")

	consoleConfig := &api.ConsoleConfig{}
	if err := config.Load(consoleConfig, envLookup.Lookup); err != nil {
		return consoleConfig, fmt.Errorf("unable to load Console configuration. %v", err)
	}

	if len(consoleConfig.AuthEndpointType) == 0 {
		//return consoleConfig, errors.New("AUTH_ENDPOINT_TYPE not found")
		//Until front-end support is implemented, default to "remote" if AUTH_ENDPOINT_TYPE is not set
		consoleConfig.AuthEndpointType = string(api.Remote)
	}

	val, endpointTypeSupported := api.AuthEndpointTypes[consoleConfig.AuthEndpointType]
	if endpointTypeSupported {
		if val == api.AuthNone {
			return consoleConfig, nil
		} else if val == api.Local {
			//Auth endpoint type is set to "local", so load the local user config
			err := initialiseLocalUsersConfiguration(consoleConfig, p)
			if err != nil {
				return consoleConfig, err
			}
		} else if val == api.Remote {
			// Auth endpoint type is set to "remote", so need to load local user config vars
			// Default authorization endpoint to be UAA endpoint
			if consoleConfig.AuthorizationEndpoint == nil {
				// No Authorization endpoint
				consoleConfig.AuthorizationEndpoint = consoleConfig.UAAEndpoint
				log.Debugf("Using UAA Endpoint for Auth Endpoint: %s", consoleConfig.AuthorizationEndpoint)
			}
		} else {
			//Auth endpoint type has been set to an invalid value
			return consoleConfig, errors.New("AUTH_ENDPOINT_TYPE must be set to either \"local\" or \"remote\"")
		}
	} else {
		return consoleConfig, errors.New("AUTH_ENDPOINT_TYPE not found")
	}

	return consoleConfig, nil
}

func initialiseLocalUsersConfiguration(consoleConfig *api.ConsoleConfig, p *portalProxy) error {

	var err error
	localUserName, found := p.Env().Lookup("LOCAL_USER")
	if !found {
		err = errors.New("LOCAL_USER not found")
	}
	localUserPassword, found := p.Env().Lookup("LOCAL_USER_PASSWORD")
	if !found {
		err = errors.New("LOCAL_USER_PASSWORD not found")
	}
	localUserScope, found := p.Env().Lookup("LOCAL_USER_SCOPE")
	if !found {
		err = errors.New("LOCAL_USER_SCOPE not found")
	}
	if err != nil {
		return err
	}

	consoleConfig.LocalUserScope = localUserScope
	consoleConfig.LocalUser = localUserName
	consoleConfig.LocalUserPassword = localUserPassword

	return setupInitialiseLocalUsersConfiguration(consoleConfig, p)
}

func setupInitialiseLocalUsersConfiguration(consoleConfig *api.ConsoleConfig, p *portalProxy) error {

	localUsersRepo, err := localusers.NewPgsqlLocalUsersRepository(p.DatabaseConnectionPool)
	if err != nil {
		log.Errorf("Unable to initialise Stratos local users config due to: %+v", err)
		return err
	}

	userGUID := uuid.NewV4().String()
	password := consoleConfig.LocalUserPassword
	passwordHash, err := crypto.HashPassword(password)
	if err != nil {
		log.Errorf("Unable to initialise Stratos local user due to: %+v", err)
		return err
	}
	scope := consoleConfig.LocalUserScope
	email := ""
	user := api.LocalUser{UserGUID: userGUID, PasswordHash: passwordHash, Username: consoleConfig.LocalUser, Email: email, Scope: scope, GivenName: "Admin", FamilyName: "User"}

	// Don't add the user if they already exist
	_, err = localUsersRepo.FindUserGUID(consoleConfig.LocalUser)
	if err == nil {
		// Can't modify the user once created else we loose any updates that might have neen made
		return nil
	}

	err = localUsersRepo.AddLocalUser(user)
	if err != nil {
		log.Errorf("Unable to add Stratos local user due to: %+v", err)
	}

	return err
}

var setupComplete = false

func (p *portalProxy) SetupMiddleware() echo.MiddlewareFunc {

	return func(h echo.HandlerFunc) echo.HandlerFunc {

		if !setupComplete {
			// Check again to see if setup is complete
			// Load the config from the database again
			setupComplete = checkSetupComplete(p)
		}

		if setupComplete {
			// No-op in case the instance has been setup
			return func(c echo.Context) error {
				return h(c)
			}
		}

		// Check URL - only let setup and vesions requests through
		return func(c echo.Context) error {
			requestURLPath := c.Request().URL.Path

			isSetupRequest, _ := regexp.MatchString(setupRequestRegex, requestURLPath)
			if !isSetupRequest {
				isSetupRequest, _ = regexp.MatchString(setupCheckRequestRegex, requestURLPath)
			}
			if isSetupRequest {
				return h(c)
			}

			isVersionRequest, _ := regexp.MatchString(versionRequestRegex, requestURLPath)
			if isVersionRequest {
				return h(c)
			}

			isPingRequest, _ := regexp.MatchString(pingRequestRegex, requestURLPath)
			if isPingRequest {
				return h(c)
			}

			// Request is not a setup request, refuse backend requests and allow all others
			isBackendRequest, _ := regexp.MatchString(backendRequestRegex, requestURLPath)
			isAPIRequest, _ := regexp.MatchString(apiRequestRegex, requestURLPath)
			if !(isBackendRequest || isAPIRequest) {
				return h(c)
			}

			// Request was a backend request other than a setup or version request
			c.Response().Header().Add("Stratos-Setup-Required", "true")
			return c.NoContent(http.StatusServiceUnavailable)
		}
	}
}

func checkSetupComplete(portalProxy *portalProxy) bool {

	consoleRepo, err := console_config.NewPostgresConsoleConfigRepository(portalProxy.DatabaseConnectionPool)
	if err != nil {
		log.Warn("Failed to connect to Database!")
		return false
	}

	// This will reload the env config
	console_config.InitializeConfEnvProvider(consoleRepo)

	// Now that the config DB is an env provider, we can just use the env to fetch the setup values
	consoleConfig, err := portalProxy.initialiseConsoleConfig(portalProxy.Env())
	if err != nil {
		log.Errorf("Unable to load console config; %+v", err)
		return false
	}

	// If setup is complete, then store the config
	if consoleConfig.IsSetupComplete() {
		showStratosConfig(portalProxy, consoleConfig)
		portalProxy.Config.ConsoleConfig = consoleConfig
		portalProxy.Config.SSOLogin = consoleConfig.UseSSO
		portalProxy.Config.AuthEndpointType = consoleConfig.AuthEndpointType
		portalProxy.InitStratosAuthService(api.AuthEndpointTypes[consoleConfig.AuthEndpointType])
	}

	return consoleConfig.IsSetupComplete()
}
