package main

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

	iop "iop/proto/gen/iop"

	"iop/apps/control-plane/internal/wire"
)

// edgeStatusRequester asks a connected Edge for its Edge-owned node snapshot.
type edgeStatusRequester func(edgeID string, timeout time.Duration) (*iop.EdgeStatusResponse, error)

// edgeCommandSender dispatches a command to a connected Edge and returns its
// typed response.
type edgeCommandSender func(edgeID, operation, targetSelector string, parameters map[string]string, timeout time.Duration) (*iop.EdgeCommandResponse, error)

func registerEdgeRegistryHandlers(mux *http.ServeMux, registry *wire.EdgeRegistry, requestStatus edgeStatusRequester, sendCommand edgeCommandSender) {
	mux.HandleFunc("/edges", func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/edges" {
			http.NotFound(w, r)
			return
		}
		if r.Method != http.MethodGet {
			methodNotAllowed(w, http.MethodGet)
			return
		}
		states := registry.Snapshots()
		views := make([]edgeRegistryView, 0, len(states))
		for _, state := range states {
			views = append(views, edgeRegistryViewFromState(state))
		}
		writeJSON(w, http.StatusOK, edgeRegistryResponse{Edges: views})
	})
	mux.HandleFunc("/edges/", func(w http.ResponseWriter, r *http.Request) {
		edgePath := strings.TrimPrefix(r.URL.Path, "/edges/")
		edgeID, rest, hasRest := strings.Cut(edgePath, "/")
		if edgeID == "" {
			http.NotFound(w, r)
			return
		}
		if !hasRest {
			if r.Method != http.MethodGet {
				methodNotAllowed(w, http.MethodGet)
				return
			}
			state, ok := registry.Snapshot(edgeID)
			if !ok {
				http.NotFound(w, r)
				return
			}
			writeJSON(w, http.StatusOK, edgeRegistryViewFromState(state))
			return
		}
		switch rest {
		case "status":
			if r.Method != http.MethodGet {
				methodNotAllowed(w, http.MethodGet)
				return
			}
			if _, ok := registry.Snapshot(edgeID); !ok {
				http.NotFound(w, r)
				return
			}
			resp, err := requestStatus(edgeID, 5*time.Second)
			if err != nil {
				writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
				return
			}
			writeJSON(w, http.StatusOK, edgeStatusResponseViewFromProto(resp))
		case "events":
			if r.Method != http.MethodGet {
				methodNotAllowed(w, http.MethodGet)
				return
			}
			if _, ok := registry.Snapshot(edgeID); !ok {
				http.NotFound(w, r)
				return
			}
			records := registry.NodeEvents(edgeID, r.URL.Query().Get("node_id"))
			views := make([]edgeNodeEventView, 0, len(records))
			for _, record := range records {
				views = append(views, edgeNodeEventViewFromRecord(record))
			}
			writeJSON(w, http.StatusOK, edgeNodeEventsResponse{EdgeID: edgeID, Events: views})
		case "operations":
			if r.Method != http.MethodGet {
				methodNotAllowed(w, http.MethodGet)
				return
			}
			if _, ok := registry.Snapshot(edgeID); !ok {
				http.NotFound(w, r)
				return
			}
			records := registry.Commands(edgeID)
			views := make([]edgeCommandRecordView, 0, len(records))
			for _, record := range records {
				views = append(views, edgeCommandRecordViewFromRecord(record))
			}
			writeJSON(w, http.StatusOK, edgeOperationsResponse{EdgeID: edgeID, Operations: views})
		case "commands":
			if r.Method != http.MethodPost {
				methodNotAllowed(w, http.MethodPost)
				return
			}
			handleEdgeCommandPost(w, r, edgeID, sendCommand)
		default:
			http.NotFound(w, r)
		}
	})
}

// handleEdgeCommandPost decodes a command request body and dispatches it to a
// single target Edge. A not-connected Edge yields a 502 with the wire error.
func handleEdgeCommandPost(w http.ResponseWriter, r *http.Request, edgeID string, sendCommand edgeCommandSender) {
	var body edgeCommandRequestBody
	if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"})
		return
	}
	if body.Operation == "" {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "operation is required"})
		return
	}
	if sendCommand == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "command dispatch is not configured"})
		return
	}
	resp, err := sendCommand(edgeID, body.Operation, body.TargetSelector, body.Parameters, 10*time.Second)
	if err != nil {
		writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
		return
	}
	writeJSON(w, http.StatusAccepted, edgeCommandResponseViewFromProto(resp))
}

func methodNotAllowed(w http.ResponseWriter, allow ...string) {
	w.Header().Set("Allow", strings.Join(allow, ", "))
	http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}

func writeJSON(w http.ResponseWriter, status int, v any) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	_ = json.NewEncoder(w).Encode(v)
}
