// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may
// not use this file except in compliance with the License. A copy of the
// License is located at
//
//      http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file 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 ipamd

import (
	"encoding/json"
	"net"
	"net/http"
	"os"
	"strings"
	"time"

	"github.com/aws/amazon-vpc-cni-k8s/pkg/eniconfig"
	"github.com/aws/amazon-vpc-cni-k8s/pkg/ipamd/datastore"
	"github.com/aws/amazon-vpc-cni-k8s/pkg/k8sapi"
	"github.com/aws/amazon-vpc-cni-k8s/pkg/networkutils"
	"github.com/aws/amazon-vpc-cni-k8s/pkg/utils/retry"
)

const (
	// defaultIntrospectionAddress is listening on localhost 61679 for ipamd introspection
	defaultIntrospectionBindAddress = "127.0.0.1:61679"

	// Environment variable to define the bind address for the introspection endpoint
	introspectionBindAddress = "INTROSPECTION_BIND_ADDRESS"
)

type rootResponse struct {
	AvailableCommands []string
}

// LoggingHandler is a object for handling http request
type LoggingHandler struct {
	h http.Handler
}

func (lh LoggingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	log.Infof("Handling http request: %s, from: %s, URI: %s", r.Method, r.RemoteAddr, r.RequestURI)
	lh.h.ServeHTTP(w, r)
}

// ServeIntrospection sets up ipamd introspection endpoints
func (c *IPAMContext) ServeIntrospection() {
	server := c.setupIntrospectionServer()
	for {
		_ = retry.WithBackoff(retry.NewSimpleBackoff(time.Second, time.Minute, 0.2, 2), func() error {
			var ln net.Listener
			var err error

			if strings.HasPrefix(server.Addr, "unix:") {
				socket := strings.TrimPrefix(server.Addr, "unix:")
				ln, err = net.Listen("unix", socket)
			} else {
				ln, err = net.Listen("tcp", server.Addr)
			}

			if err == nil {
				err = server.Serve(ln)
			}

			return err
		})
	}
}

func (c *IPAMContext) setupIntrospectionServer() *http.Server {
	serverFunctions := map[string]func(w http.ResponseWriter, r *http.Request){
		"/v1/enis":                      eniV1RequestHandler(c),
		"/v1/eni-configs":               eniConfigRequestHandler(c),
		"/v1/networkutils-env-settings": networkEnvV1RequestHandler(),
		"/v1/ipamd-env-settings":        ipamdEnvV1RequestHandler(),
	}
	paths := make([]string, 0, len(serverFunctions))
	for path := range serverFunctions {
		paths = append(paths, path)
	}
	availableCommands := &rootResponse{paths}
	// Autogenerated list of the above serverFunctions paths
	availableCommandResponse, err := json.Marshal(&availableCommands)

	if err != nil {
		log.Errorf("Failed to marshal: %v", err)
	}

	defaultHandler := func(w http.ResponseWriter, r *http.Request) {
		logErr(w.Write(availableCommandResponse))
	}
	serveMux := http.NewServeMux()
	serveMux.HandleFunc("/", defaultHandler)
	for key, fn := range serverFunctions {
		serveMux.HandleFunc(key, fn)
	}

	// Log all requests and then pass through to serveMux
	loggingServeMux := http.NewServeMux()
	loggingServeMux.Handle("/", LoggingHandler{serveMux})

	addr, ok := os.LookupEnv(introspectionBindAddress)
	if !ok {
		addr = defaultIntrospectionBindAddress
	}

	log.Infof("Serving introspection endpoints on %s", addr)

	server := &http.Server{
		Addr:         addr,
		Handler:      loggingServeMux,
		ReadTimeout:  5 * time.Second,
		WriteTimeout: 5 * time.Second,
	}
	return server
}

func eniV1RequestHandler(ipam *IPAMContext) func(http.ResponseWriter, *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		eniInfos := make(map[int]*datastore.ENIInfos, len(ipam.dataStoreAccess.DataStores))
		for _, ds := range ipam.dataStoreAccess.DataStores {
			eniInfos[ds.GetNetworkCard()] = ds.GetENIInfos()
		}
		responseJSON, err := json.Marshal(eniInfos)
		if err != nil {
			log.Errorf("Failed to marshal ENI data: %v", err)
			http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
			return
		}
		logErr(w.Write(responseJSON))
	}
}

func eniConfigRequestHandler(ipam *IPAMContext) func(http.ResponseWriter, *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		node, err := k8sapi.GetNode(ctx, ipam.k8sClient)
		if err != nil {
			log.Errorf("Failed to get host node: %v", err)
			http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
			return
		}
		myENIConfig, err := eniconfig.GetNodeSpecificENIConfigName(node)
		if err != nil {
			log.Errorf("Failed to get ENI config: %v", err)
			http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
			return
		}
		responseJSON, err := json.Marshal(myENIConfig)
		if err != nil {
			log.Errorf("Failed to marshal ENI config: %v", err)
			http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
			return
		}
		logErr(w.Write(responseJSON))
	}
}

func networkEnvV1RequestHandler() func(http.ResponseWriter, *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		responseJSON, err := json.Marshal(networkutils.GetConfigForDebug())
		if err != nil {
			log.Errorf("Failed to marshal network env var data: %v", err)
			http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
			return
		}
		logErr(w.Write(responseJSON))
	}
}

func ipamdEnvV1RequestHandler() func(http.ResponseWriter, *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		responseJSON, err := json.Marshal(GetConfigForDebug())
		if err != nil {
			log.Errorf("Failed to marshal ipamd env var data: %v", err)
			http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
			return
		}
		logErr(w.Write(responseJSON))
	}
}

func logErr(_ int, err error) {
	if err != nil {
		log.Errorf("Write failed: %v", err)
	}
}
