package ring

import (
	"context"
	_ "embed"
	"fmt"
	"html/template"
	"math"
	"net/http"
	"slices"
	"sort"
	"strconv"
	"time"
)

//go:embed partition_ring_status.gohtml
var partitionRingPageContent string
var partitionRingPageTemplate = template.Must(template.New("webpage").Funcs(template.FuncMap{
	"mod": func(i, j int32) bool {
		return i%j == 0
	},
	"humanFloat": func(f float64) string {
		return fmt.Sprintf("%.3g", f)
	},
	"formatTimestamp": func(ts time.Time) string {
		return ts.Format("2006-01-02 15:04:05 MST")
	},
}).Parse(partitionRingPageContent))

type PartitionRingUpdater interface {
	ChangePartitionState(ctx context.Context, partitionID int32, toState PartitionState) error
	SetPartitionStateChangeLock(ctx context.Context, partitionID int32, locked bool) error
}

type PartitionRingPageHandler struct {
	reader  PartitionRingReader
	updater PartitionRingUpdater
}

func NewPartitionRingPageHandler(reader PartitionRingReader, updater PartitionRingUpdater) *PartitionRingPageHandler {
	return &PartitionRingPageHandler{
		reader:  reader,
		updater: updater,
	}
}

func (h *PartitionRingPageHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	switch req.Method {
	case http.MethodGet:
		h.handleGetRequest(w, req)
	case http.MethodPost:
		h.handlePostRequest(w, req)
	default:
		http.Error(w, "Unsupported HTTP method", http.StatusMethodNotAllowed)
	}
}

func (h *PartitionRingPageHandler) handleGetRequest(w http.ResponseWriter, req *http.Request) {
	var (
		ring     = h.reader.PartitionRing()
		ringDesc = ring.desc
	)
	ownedTokens := ringDesc.countTokens()

	// Prepare the data to render partitions in the page.
	partitionsByID := make(map[int32]partitionPageData, len(ringDesc.Partitions))
	for id, partition := range ringDesc.Partitions {
		owners := ring.PartitionOwnerIDsCopy(id)
		slices.Sort(owners)

		partitionsByID[id] = partitionPageData{
			ID:                id,
			Corrupted:         false,
			State:             partition.State,
			StateTimestamp:    partition.GetStateTime(),
			StateChangeLocked: partition.StateChangeLocked,
			OwnerIDs:          owners,
			Tokens:            partition.Tokens,
			NumTokens:         len(partition.Tokens),
			Ownership:         distancePercentage(ownedTokens[id]),
		}
	}

	// Look for owners of non-existing partitions. We want to provide visibility for such case
	// and we report the partition in corrupted state.
	for ownerID, owner := range ringDesc.Owners {
		partition, exists := partitionsByID[owner.OwnedPartition]

		if !exists {
			partition = partitionPageData{
				ID:             owner.OwnedPartition,
				Corrupted:      true,
				State:          PartitionUnknown,
				StateTimestamp: time.Time{},
				OwnerIDs:       []string{ownerID},
				Tokens:         partition.Tokens,
				NumTokens:      len(partition.Tokens),
				Ownership:      distancePercentage(ownedTokens[owner.OwnedPartition]),
			}

			partitionsByID[owner.OwnedPartition] = partition
		}

		if !slices.Contains(partition.OwnerIDs, ownerID) {
			partition.OwnerIDs = append(partition.OwnerIDs, ownerID)
			partitionsByID[owner.OwnedPartition] = partition
		}
	}

	// Covert partitions to a list and sort it by ID.
	partitions := make([]partitionPageData, 0, len(partitionsByID))

	for _, partition := range partitionsByID {
		partitions = append(partitions, partition)
	}

	sort.Slice(partitions, func(i, j int) bool {
		return partitions[i].ID < partitions[j].ID
	})

	tokensParam := req.URL.Query().Get("tokens")

	renderHTTPResponse(w, partitionRingPageData{
		Partitions: partitions,
		PartitionStateChanges: map[PartitionState]PartitionState{
			PartitionPending:  PartitionActive,
			PartitionActive:   PartitionInactive,
			PartitionInactive: PartitionActive,
		},
		ShowTokens: tokensParam == "true",
	}, partitionRingPageTemplate, req)
}

func (h *PartitionRingPageHandler) handlePostRequest(w http.ResponseWriter, req *http.Request) {
	if req.FormValue("action") == "change_state" {
		partitionID, err := strconv.Atoi(req.FormValue("partition_id"))
		if err != nil {
			http.Error(w, fmt.Sprintf("invalid partition ID: %s", err.Error()), http.StatusBadRequest)
			return
		}

		toState, ok := PartitionState_value[req.FormValue("partition_state")]
		if !ok {
			http.Error(w, "invalid partition state", http.StatusBadRequest)
			return
		}

		if err := h.updater.ChangePartitionState(req.Context(), int32(partitionID), PartitionState(toState)); err != nil {
			http.Error(w, fmt.Sprintf("failed to change partition state: %s", err.Error()), http.StatusBadRequest)
			return
		}
	} else if req.FormValue("action") == "change_state_and_lock" {
		// NOTE: To avoid playing Whac-a-Mole with rollout-operator (or other actors) reverting the state to active BEFORE the operator
		// is able to lock the partition state change, we offer this method which attempts to change the state and lock immediately.
		// This currently contains a race. But since usually this endpoint is served by many replicas, fixing the race would require
		// some work. We believe it's not worth it to add the additional complexity and we rely on the user using this to ensure
		// that the state change is locked in the desired state.
		partitionID, err := strconv.Atoi(req.FormValue("partition_id"))
		if err != nil {
			http.Error(w, fmt.Sprintf("invalid partition ID: %s", err.Error()), http.StatusBadRequest)
			return
		}

		toState, ok := PartitionState_value[req.FormValue("partition_state")]
		if !ok {
			http.Error(w, "invalid partition state", http.StatusBadRequest)
			return
		}

		if err := h.updater.ChangePartitionState(req.Context(), int32(partitionID), PartitionState(toState)); err != nil {
			http.Error(w, fmt.Sprintf("failed to change partition state: %s", err.Error()), http.StatusBadRequest)
			return
		}

		if err := h.updater.SetPartitionStateChangeLock(req.Context(), int32(partitionID), true); err != nil {
			http.Error(w, fmt.Sprintf("failed to lock partition state change: %s", err.Error()), http.StatusBadRequest)
			return
		}
	} else if req.FormValue("action") == "state_change_lock" {
		partitionID, err := strconv.Atoi(req.FormValue("partition_id"))
		if err != nil {
			http.Error(w, fmt.Sprintf("invalid partition ID: %s", err.Error()), http.StatusBadRequest)
			return
		}

		locked, err := strconv.ParseBool(req.FormValue("locked"))
		if err != nil {
			http.Error(w, fmt.Sprintf("invalid locked value: %s", err.Error()), http.StatusBadRequest)
			return
		}

		if err := h.updater.SetPartitionStateChangeLock(req.Context(), int32(partitionID), locked); err != nil {
			http.Error(w, fmt.Sprintf("failed to lock partition state change: %s", err.Error()), http.StatusBadRequest)
			return
		}
	}

	// Implement PRG pattern to prevent double-POST and work with CSRF middleware.
	// https://en.wikipedia.org/wiki/Post/Redirect/Get
	w.Header().Set("Location", "#")
	w.WriteHeader(http.StatusFound)
}

type partitionRingPageData struct {
	Partitions []partitionPageData `json:"partitions"`

	// PartitionStateChanges maps the allowed state changes through the UI.
	PartitionStateChanges map[PartitionState]PartitionState `json:"-"`
	ShowTokens            bool                              `json:"-"`
}

type partitionPageData struct {
	ID                int32          `json:"id"`
	Corrupted         bool           `json:"corrupted"`
	State             PartitionState `json:"state"`
	StateTimestamp    time.Time      `json:"state_timestamp"`
	StateChangeLocked bool           `json:"state_change_locked"`
	OwnerIDs          []string       `json:"owner_ids"`
	Tokens            []uint32       `json:"tokens"`
	NumTokens         int            `json:"-"`
	Ownership         float64        `json:"-"`
}

// distancePercentage renders a given token distance as the percentage of all possible token values covered by that distance.
func distancePercentage(distance int64) float64 {
	return (float64(distance) / float64(math.MaxUint32)) * 100
}
