/*
Copyright 2018 The Argoproj Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

	http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package webhook

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

	"github.com/gorilla/mux"
	"go.uber.org/zap"

	"github.com/argoproj/argo-events/pkg/apis/events/v1alpha1"
	eventsourcecommon "github.com/argoproj/argo-events/pkg/eventsources/common"
	metrics "github.com/argoproj/argo-events/pkg/metrics"
	"github.com/argoproj/argo-events/pkg/shared/logging"
	sharedutil "github.com/argoproj/argo-events/pkg/shared/util"
)

// NewController returns a webhook controller
func NewController() *Controller {
	return &Controller{
		AllRoutes:            make(map[string]*mux.Route),
		ActiveServerHandlers: make(map[string]*mux.Router),
		RouteActivateChan:    make(chan Router),
		RouteDeactivateChan:  make(chan Router),
	}
}

// NewRoute returns a vanilla route
func NewRoute(hookContext *v1alpha1.WebhookContext, logger *zap.SugaredLogger, eventSourceName, eventName string, metrics *metrics.Metrics) *Route {
	return &Route{
		Context:         hookContext,
		Logger:          logger,
		EventSourceName: eventSourceName,
		EventName:       eventName,
		Active:          false,
		DispatchChan:    make(chan *Dispatch),
		StartCh:         make(chan struct{}),
		StopChan:        make(chan struct{}),
		Metrics:         metrics,
	}
}

func DispatchEvent(route *Route, data []byte, logger *zap.SugaredLogger, writer http.ResponseWriter) {
	logger.Info("dispatching event on route's dispatch channel...")
	successChan := make(chan bool)
	route.DispatchChan <- &Dispatch{Data: data, SuccessChan: successChan}
	if <-successChan {
		logger.Info("successfully dispatched the request to the event bus")
		sharedutil.SendSuccessResponse(writer, "success")
	} else {
		logger.Error("failed to dispatch the request to the event bus")
		sharedutil.SendInternalErrorResponse(writer, "failed to record event")
	}
}

// ProcessRouteStatus processes route status as active and inactive.
func ProcessRouteStatus(ctrl *Controller) {
	for {
		select {
		case router := <-ctrl.RouteActivateChan:
			// start server if it has not been started on this port
			startServer(router, ctrl)
			// to allow route process incoming requests
			router.GetRoute().StartCh <- struct{}{}

		case router := <-ctrl.RouteDeactivateChan:
			router.GetRoute().Active = false
		}
	}
}

// starts a http server
func startServer(router Router, controller *Controller) {
	// start a http server only if no other configuration previously started the server on given port
	Lock.Lock()
	route := router.GetRoute()
	if _, ok := controller.ActiveServerHandlers[route.Context.Port]; !ok {
		handler := mux.NewRouter()
		server := &http.Server{
			Addr:    fmt.Sprintf(":%s", route.Context.Port),
			Handler: handler,
		}

		controller.ActiveServerHandlers[route.Context.Port] = handler

		// start http server
		go func() {
			switch {
			case route.Context.ServerCertSecret != nil && route.Context.ServerKeySecret != nil:
				certPath, err := sharedutil.GetSecretVolumePath(route.Context.ServerCertSecret)
				if err != nil {
					route.Logger.Errorw("failed to get cert path in mounted volume", "error", err)
					return
				}
				keyPath, err := sharedutil.GetSecretVolumePath(route.Context.ServerKeySecret)
				if err != nil {
					route.Logger.Errorw("failed to get key path in mounted volume", "error", err)
					return
				}
				err = server.ListenAndServeTLS(certPath, keyPath)
				if err != nil {
					route.Logger.With("port", route.Context.Port).Errorw("failed to listen and serve with TLS configured", zap.Error(err))
				}
			default:
				err := server.ListenAndServe()
				if err != nil {
					route.Logger.With("port", route.Context.Port).Errorw("failed to listen and serve", zap.Error(err))
				}
			}
		}()
	}

	handler := controller.ActiveServerHandlers[route.Context.Port]

	routeName := route.Context.Port + route.Context.Endpoint
	r := handler.GetRoute(routeName)
	if r == nil {
		r = handler.NewRoute().Name(routeName)
		r = r.Path(route.Context.Endpoint)
		r.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
			if route.Context.AuthSecret != nil {
				token, err := sharedutil.GetSecretFromVolume(route.Context.AuthSecret)
				if err != nil {
					route.Logger.Errorw("failed to get auth secret from volume", "error", err)
					sharedutil.SendInternalErrorResponse(writer, "Error loading auth token")
					return
				}
				authHeader := request.Header.Get("Authorization")
				if !strings.HasPrefix(authHeader, "Bearer ") {
					route.Logger.Error("invalid auth header")
					sharedutil.SendResponse(writer, http.StatusUnauthorized, "Invalid Authorization Header")
					return
				}
				if strings.TrimPrefix(authHeader, "Bearer ") != token {
					route.Logger.Error("invalid auth token")
					sharedutil.SendResponse(writer, http.StatusUnauthorized, "Invalid Auth token")
					return
				}
			}
			if request.Header.Get("Authorization") != "" {
				// Auth secret stops here
				request.Header.Set("Authorization", "*** Masked Auth Secret ***")
			}
			router.HandleRoute(writer, request)
		})
	}

	healthCheckRouteName := route.Context.Port + "/health"
	healthCheckRoute := handler.GetRoute(healthCheckRouteName)
	if healthCheckRoute == nil {
		healthCheckRoute = handler.NewRoute().Name(healthCheckRouteName)
		healthCheckRoute = healthCheckRoute.Path("/health")
		healthCheckRoute.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
			sharedutil.SendSuccessResponse(writer, "OK")
		})
	}

	Lock.Unlock()
}

// activateRoute activates a route to process incoming requests
func activateRoute(router Router, controller *Controller) {
	route := router.GetRoute()
	// change status of route as a active route
	controller.RouteActivateChan <- router

	// wait for any route to become ready
	// if this is the first route that is added for a server, then controller will
	// start a http server before marking the route as ready
	<-route.StartCh

	route.Active = true
	route.Logger.With(logging.LabelPort, route.Context.Port, logging.LabelEndpoint, route.Context.Endpoint).Info("route is activated")
}

// manageRouteChannels consumes data from route's data channel and stops the processing when the event source is stopped/removed
func manageRouteChannels(router Router, dispatch func([]byte, ...eventsourcecommon.Option) error) {
	route := router.GetRoute()
	logger := route.Logger
	for {
		select {
		case dispatchStruct := <-route.DispatchChan:
			logger.Info("new event received, dispatching it...")
			if err := dispatch(dispatchStruct.Data); err != nil {
				logger.Errorw("failed to send event", zap.Error(err))
				dispatchStruct.SuccessChan <- false
				route.Metrics.EventProcessingFailed(route.EventSourceName, route.EventName)
				continue
			}
			dispatchStruct.SuccessChan <- true

		case <-route.StopChan:
			logger.Info("event source is stopped")
			return
		}
	}
}

// ManagerRoute manages the lifecycle of a route
func ManageRoute(ctx context.Context, router Router, controller *Controller, dispatch func([]byte, ...eventsourcecommon.Option) error) error {
	route := router.GetRoute()

	logger := route.Logger

	// in order to process a route, it needs to go through
	// 1. validation - basic configuration checks
	// 2. activation - associate http handler if not done previously
	// 3. post start operations - operations that must be performed after route has been activated and ready to process requests
	// 4. consume data from route's data channel
	// 5. post stop operations - operations that must be performed after route is inactivated

	logger.Info("validating the route...")
	if err := validateRoute(router.GetRoute()); err != nil {
		logger.Error("route is invalid, won't initialize it", zap.Error(err))
		return err
	}

	logger.Info("listening to payloads for the route...")
	go manageRouteChannels(router, dispatch)

	defer func() {
		route.StopChan <- struct{}{}
	}()

	logger.Info("activating the route...")
	activateRoute(router, controller)

	logger.Info("running operations post route activation...")
	if err := router.PostActivate(); err != nil {
		logger.Errorw("error occurred while performing post route activation operations", zap.Error(err))
		return err
	}

	<-ctx.Done()
	logger.Info("connection is closed by client")

	logger.Info("marking route as inactive")
	controller.RouteDeactivateChan <- router

	logger.Info("running operations post route inactivation...")
	if err := router.PostInactivate(); err != nil {
		logger.Errorw("error occurred while running operations post route inactivation", zap.Error(err))
	}

	return nil
}
