// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package hcp

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"regexp"
	"strings"
	"sync"
	"time"

	hcpgnm "github.com/hashicorp/hcp-sdk-go/clients/cloud-global-network-manager-service/preview/2022-02-15/client/global_network_manager_service"
	gnmmod "github.com/hashicorp/hcp-sdk-go/clients/cloud-global-network-manager-service/preview/2022-02-15/models"
	"github.com/hashicorp/hcp-sdk-go/resource"
)

type TestEndpoint struct {
	Methods    []string
	PathSuffix string
	Handler    func(r *http.Request, cluster resource.Resource) (interface{}, error)
}

type MockHCPServer struct {
	mu       sync.Mutex
	handlers map[string]TestEndpoint

	servers map[string]*gnmmod.HashicorpCloudGlobalNetworkManager20220215Server
}

var basePathRe = regexp.MustCompile("/global-network-manager/[^/]+/organizations/([^/]+)/projects/([^/]+)/clusters/([^/]+)/([^/]+.*)")

func NewMockHCPServer() *MockHCPServer {
	s := &MockHCPServer{
		handlers: make(map[string]TestEndpoint),
		servers:  make(map[string]*gnmmod.HashicorpCloudGlobalNetworkManager20220215Server),
	}
	// Define endpoints in this package
	s.AddEndpoint(TestEndpoint{
		Methods:    []string{"POST"},
		PathSuffix: "agent/server-state",
		Handler:    s.handleStatus,
	})
	s.AddEndpoint(TestEndpoint{
		Methods:    []string{"POST"},
		PathSuffix: "agent/discover",
		Handler:    s.handleDiscover,
	})
	return s
}

// AddEndpoint allows adding additional endpoints from other packages e.g.
// bootstrap (which can't be merged into one package due to dependency cycles).
// It's not safe to call this concurrently with any other call to AddEndpoint or
// ServeHTTP.
func (s *MockHCPServer) AddEndpoint(e TestEndpoint) {
	s.handlers[e.PathSuffix] = e
}

func (s *MockHCPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	s.mu.Lock()
	defer s.mu.Unlock()

	if r.URL.Path == "/oauth2/token" {
		mockTokenResponse(w)
		return
	}

	matches := basePathRe.FindStringSubmatch(r.URL.Path)
	if matches == nil || len(matches) < 5 {
		w.WriteHeader(404)
		log.Printf("ERROR 404: %s %s\n", r.Method, r.URL.Path)
		return
	}

	cluster := resource.Resource{
		ID:           matches[3],
		Type:         "cluster",
		Organization: matches[1],
		Project:      matches[2],
	}
	found := false
	var resp interface{}
	var err error
	for _, e := range s.handlers {
		if e.PathSuffix == matches[4] {
			found = true
			if !enforceMethod(w, r, e.Methods) {
				return
			}
			resp, err = e.Handler(r, cluster)
			break
		}
	}
	if !found {
		w.WriteHeader(404)
		log.Printf("ERROR 404: %s %s\n", r.Method, r.URL.Path)
		return
	}
	if err != nil {
		errResponse(w, err)
		return
	}

	if resp == nil {
		// no response body
		log.Printf("OK 204: %s %s\n", r.Method, r.URL.Path)
		w.WriteHeader(http.StatusNoContent)
		return
	}

	bs, err := json.MarshalIndent(resp, "", "  ")
	if err != nil {
		errResponse(w, err)
		return
	}

	log.Printf("OK 200: %s %s\n", r.Method, r.URL.Path)
	w.Header().Set("content-type", "application/json")
	w.WriteHeader(http.StatusOK)
	w.Write(bs)
}

func enforceMethod(w http.ResponseWriter, r *http.Request, methods []string) bool {
	for _, m := range methods {
		if strings.EqualFold(r.Method, m) {
			return true
		}
	}
	// No match, sent 4xx
	w.WriteHeader(http.StatusMethodNotAllowed)
	log.Printf("ERROR 405: bad method (not in %v): %s %s\n", methods, r.Method, r.URL.Path)
	return false
}

func mockTokenResponse(w http.ResponseWriter) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)

	w.Write([]byte(`{"access_token": "token", "token_type": "Bearer"}`))
}

func (s *MockHCPServer) handleStatus(r *http.Request, cluster resource.Resource) (interface{}, error) {
	var req hcpgnm.AgentPushServerStateBody
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		return nil, err
	}
	log.Printf("STATUS UPDATE: server=%s version=%s leader=%v hasLeader=%v healthy=%v tlsCertExpiryDays=%1.0f",
		req.ServerState.Name,
		req.ServerState.Version,
		req.ServerState.Raft.IsLeader,
		req.ServerState.Raft.KnownLeader,
		req.ServerState.Autopilot.Healthy,
		time.Until(time.Time(req.ServerState.ServerTLS.InternalRPC.CertExpiry)).Hours()/24,
	)
	s.servers[req.ServerState.Name] = &gnmmod.HashicorpCloudGlobalNetworkManager20220215Server{
		GossipPort: req.ServerState.GossipPort,
		ID:         req.ServerState.ID,
		LanAddress: req.ServerState.LanAddress,
		Name:       req.ServerState.Name,
		RPCPort:    req.ServerState.RPCPort,
	}
	return "{}", nil
}

func (s *MockHCPServer) handleDiscover(r *http.Request, cluster resource.Resource) (interface{}, error) {
	servers := make([]*gnmmod.HashicorpCloudGlobalNetworkManager20220215Server, len(s.servers))
	for _, server := range s.servers {
		servers = append(servers, server)
	}

	return gnmmod.HashicorpCloudGlobalNetworkManager20220215AgentDiscoverResponse{Servers: servers}, nil
}

func errResponse(w http.ResponseWriter, err error) {
	log.Printf("ERROR 500: %s\n", err)
	w.WriteHeader(500)
	w.Write([]byte(fmt.Sprintf(`{"error": %q}`, err.Error())))
}
