package main

import (
	"encoding/json"
	"errors"
	"io"
	"net/http"
	"strconv"
	"strings"

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

const maxProviderSecretBytes = 64 << 10

// registerCredentialHandlers exposes only principal-authenticated slot/route
// lifecycle operations. Production mounts it solely on the credential HTTPS
// listener; principal bootstrap remains an offline CLI operation.
func registerCredentialHandlers(mux *http.ServeMux, service *credentialops.Service, onMutation func(*http.Request) error) {
	mux.Handle("/v1/credentials/slots", credentialCollectionHandler{service: service, onMutation: onMutation})
	mux.Handle("/v1/credentials/slots/", credentialSlotHandler{service: service, onMutation: onMutation})
	mux.Handle("/v1/credentials/routes", credentialRouteCollectionHandler{service: service, onMutation: onMutation})
	mux.Handle("/v1/credentials/routes/", credentialRouteHandler{service: service, onMutation: onMutation})
}

func bearerToken(r *http.Request) ([]byte, bool) {
	value := strings.TrimSpace(r.Header.Get("Authorization"))
	if len(value) < 8 || !strings.EqualFold(value[:7], "Bearer ") || strings.TrimSpace(value[7:]) == "" {
		return nil, false
	}
	return []byte(strings.TrimSpace(value[7:])), true
}

func readSecret(w http.ResponseWriter, r *http.Request) ([]byte, bool) {
	if r.Header.Get("Content-Type") != "application/octet-stream" {
		http.Error(w, "application/octet-stream is required", http.StatusUnsupportedMediaType)
		return nil, false
	}
	limited := io.LimitReader(r.Body, maxProviderSecretBytes+1)
	secret, err := io.ReadAll(limited)
	if err != nil || len(secret) == 0 || len(secret) > maxProviderSecretBytes {
		zeroHTTPSecret(secret)
		http.Error(w, "provider secret must be 1..65536 bytes", http.StatusRequestEntityTooLarge)
		return nil, false
	}
	return secret, true
}

type credentialCollectionHandler struct {
	service    *credentialops.Service
	onMutation func(*http.Request) error
}

func (h credentialCollectionHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	token, ok := bearerToken(r)
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}
	defer zeroHTTPSecret(token)
	switch r.Method {
	case http.MethodGet:
		result, err := h.service.ListSlots(r.Context(), token)
		writeCredentialResult(w, result, err)
	case http.MethodPost:
		secret, ok := readSecret(w, r)
		if !ok {
			return
		}
		defer zeroHTTPSecret(secret)
		result, err := h.service.CreateSlot(r.Context(), token, credentialops.CreateSlotInput{
			Vendor: r.Header.Get("IOP-Credential-Vendor"), CredentialKind: r.Header.Get("IOP-Credential-Kind"),
			Alias: r.Header.Get("IOP-Credential-Alias"), ProviderSecret: secret,
		})
		writeCredentialMutationResult(w, r, result, err, h.onMutation)
	default:
		w.WriteHeader(http.StatusMethodNotAllowed)
	}
}

type credentialSlotHandler struct {
	service    *credentialops.Service
	onMutation func(*http.Request) error
}

func (h credentialSlotHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	token, ok := bearerToken(r)
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}
	defer zeroHTTPSecret(token)
	path := strings.TrimPrefix(r.URL.Path, "/v1/credentials/slots/")
	parts := strings.Split(path, "/")
	if len(parts) != 2 || r.Method != http.MethodPost {
		http.NotFound(w, r)
		return
	}
	revision, err := strconv.ParseInt(r.Header.Get("IOP-Expected-Revision"), 10, 64)
	if err != nil || revision < 0 {
		http.Error(w, "valid IOP-Expected-Revision is required", http.StatusBadRequest)
		return
	}
	var result any
	var callErr error
	switch parts[1] {
	case "rotate":
		secret, ok := readSecret(w, r)
		if !ok {
			return
		}
		defer zeroHTTPSecret(secret)
		result, callErr = h.service.RotateSlot(r.Context(), token, credentialops.RotateSlotInput{SlotID: parts[0], Revision: revision, ProviderSecret: secret})
	case "disable":
		result, callErr = h.service.DisableSlot(r.Context(), token, parts[0], revision)
	case "enable":
		result, callErr = h.service.EnableSlot(r.Context(), token, parts[0], revision)
	case "revoke":
		result, callErr = h.service.RevokeSlot(r.Context(), token, parts[0], revision)
	default:
		http.NotFound(w, r)
		return
	}
	writeCredentialMutationResult(w, r, result, callErr, h.onMutation)
}

type credentialRouteCollectionHandler struct {
	service    *credentialops.Service
	onMutation func(*http.Request) error
}

func (h credentialRouteCollectionHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	token, ok := bearerToken(r)
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}
	defer zeroHTTPSecret(token)
	switch r.Method {
	case http.MethodGet:
		result, err := h.service.ListRoutes(r.Context(), token)
		writeCredentialResult(w, result, err)
	case http.MethodPost:
		if contentType := r.Header.Get("Content-Type"); contentType != "application/json" {
			http.Error(w, "application/json is required", http.StatusUnsupportedMediaType)
			return
		}
		var payload struct {
			SlotID           string `json:"slot_id"`
			Alias            string `json:"alias"`
			ProfileID        string `json:"profile_id"`
			UpstreamModel    string `json:"upstream_model"`
			ResourceSelector string `json:"resource_selector"`
		}
		dec := json.NewDecoder(io.LimitReader(r.Body, 64<<10))
		dec.DisallowUnknownFields()
		if err := dec.Decode(&payload); err != nil {
			http.Error(w, "invalid route request", http.StatusBadRequest)
			return
		}
		result, err := h.service.CreateRoute(r.Context(), token, credentialops.CreateRouteInput{
			SlotID: payload.SlotID, Alias: payload.Alias, ProfileID: payload.ProfileID,
			UpstreamModel: payload.UpstreamModel, ResourceSelector: payload.ResourceSelector,
		})
		writeCredentialMutationResult(w, r, result, err, h.onMutation)
	default:
		w.WriteHeader(http.StatusMethodNotAllowed)
	}
}

type credentialRouteHandler struct {
	service    *credentialops.Service
	onMutation func(*http.Request) error
}

func (h credentialRouteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	token, ok := bearerToken(r)
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}
	defer zeroHTTPSecret(token)
	parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/v1/credentials/routes/"), "/")
	if len(parts) != 2 || r.Method != http.MethodPost {
		http.NotFound(w, r)
		return
	}
	revision, err := strconv.ParseInt(r.Header.Get("IOP-Expected-Revision"), 10, 64)
	if err != nil || revision < 0 {
		http.Error(w, "valid IOP-Expected-Revision is required", http.StatusBadRequest)
		return
	}
	var result any
	var callErr error
	switch parts[1] {
	case "update":
		if contentType := r.Header.Get("Content-Type"); contentType != "application/json" {
			http.Error(w, "application/json is required", http.StatusUnsupportedMediaType)
			return
		}
		var payload struct {
			SlotID           string `json:"slot_id"`
			Alias            string `json:"alias"`
			ProfileID        string `json:"profile_id"`
			UpstreamModel    string `json:"upstream_model"`
			ResourceSelector string `json:"resource_selector"`
		}
		dec := json.NewDecoder(io.LimitReader(r.Body, 64<<10))
		dec.DisallowUnknownFields()
		if err := dec.Decode(&payload); err != nil {
			http.Error(w, "invalid route request", http.StatusBadRequest)
			return
		}
		result, callErr = h.service.UpdateRoute(r.Context(), token, credentialops.UpdateRouteInput{
			RouteID: parts[0], CurrentRevision: revision, SlotID: payload.SlotID,
			Alias: payload.Alias, ProfileID: payload.ProfileID, UpstreamModel: payload.UpstreamModel,
			ResourceSelector: payload.ResourceSelector,
		})
	case "disable":
		result, callErr = h.service.DisableRoute(r.Context(), token, parts[0], revision)
	case "enable":
		result, callErr = h.service.EnableRoute(r.Context(), token, parts[0], revision)
	case "revoke":
		result, callErr = h.service.RevokeRoute(r.Context(), token, parts[0], revision)
	default:
		http.NotFound(w, r)
		return
	}
	writeCredentialMutationResult(w, r, result, callErr, h.onMutation)
}

func writeCredentialMutationResult(w http.ResponseWriter, r *http.Request, result any, err error, onMutation func(*http.Request) error) {
	if err == nil && onMutation != nil {
		err = onMutation(r)
	}
	writeCredentialResult(w, result, err)
}

func writeCredentialResult(w http.ResponseWriter, result any, err error) {
	if err != nil {
		status := http.StatusBadRequest
		if errors.Is(err, credentialops.ErrUnauthorized) {
			status = http.StatusUnauthorized
		} else if errors.Is(err, credentialops.ErrNotFound) {
			status = http.StatusNotFound
		} else if errors.Is(err, credentialops.ErrStaleRevision) {
			status = http.StatusConflict
		}
		http.Error(w, http.StatusText(status), status)
		return
	}
	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(result)
}

func zeroHTTPSecret(value []byte) {
	for i := range value {
		value[i] = 0
	}
}
