package frontend

import (
	"context"
	"fmt"
	"net/http"
	"sort"
	"strconv"

	"github.com/Jim3Things/CloudChamber/simulation/pkg/errors/HTTP"
	"github.com/gorilla/sessions"

	"github.com/gorilla/mux"

	"github.com/Jim3Things/CloudChamber/simulation/internal/clients/inventory"
	"github.com/Jim3Things/CloudChamber/simulation/internal/clients/timestamp"
	"github.com/Jim3Things/CloudChamber/simulation/internal/common"
	"github.com/Jim3Things/CloudChamber/simulation/internal/tracing"
	"github.com/Jim3Things/CloudChamber/simulation/pkg/errors"
	pb "github.com/Jim3Things/CloudChamber/simulation/pkg/protos/inventory"
)

func inventoryAddRoutes(routeBase *mux.Router) {

	routeRacks := routeBase.PathPrefix("/racks").Subrouter()

	routeRacks.HandleFunc("", handlerRacksList).Methods("GET")
	routeRacks.HandleFunc("/", handlerRacksList).Methods("GET")

	routeRacks.HandleFunc("/{rackID}", handlerRackRead).Methods("GET")
	routeRacks.HandleFunc("/{rackID}/", handlerRackRead).Methods("GET")
	routeRacks.HandleFunc("/{rackID}/blades", handlerBladesList).Methods("GET")
	routeRacks.HandleFunc("/{rackID}/blades/", handlerBladesList).Methods("GET")
	routeRacks.HandleFunc("/{rackID}/blades/{bladeID}", handlerBladeRead).Methods("GET")

}

func handlerRacksList(w http.ResponseWriter, r *http.Request) {
	ctx, span := tracing.StartSpan(context.Background(),
		tracing.WithName("Get Cluster Inventory List of Racks"),
		tracing.WithContextValue(timestamp.EnsureTickInContext),
		tracing.AsInternal())
	defer span.End()

	err := doSessionHeader(
		ctx, w, r,
		func(_ context.Context, session *sessions.Session) error {
			return WS.sessions.ensureEstablishedSession(session)
		})

	if err != nil {
		HTTP.PostError(ctx, w, err)
		return
	}

	w.Header().Set("Content-Type", "application/json")

	memoData := WS.Inventory.GetMemoData()
	if memoData == nil {
		HTTP.PostError(ctx, w, errors.ErrInventoryNotAvailable)
		return
	}

	zoneDetails, err := WS.Inventory.GetZoneDetails(ctx, inventory.DefaultRegion, inventory.DefaultZone)
	if err != nil {
		HTTP.PostError(ctx, w, errors.ErrInventoryNotAvailable)
		return
	}

	racks := make([]*pb.External_RackSummary, 0, memoData.RackCount)

	res := &pb.External_ZoneSummary{
		MaxBladeCount: int64(memoData.MaxBladeCount),
		MaxCapacity:   memoData.MaxCapacity,
		MaxTorCount:   int64(memoData.MaxTorCount),
		MaxPduCount:   int64(memoData.MaxPduCount),
		MaxConnectors: int64(memoData.MaxConnectors),
		Name:          inventory.DefaultZone,
		Details:       zoneDetails,
	}

	tracing.Info(
		ctx,
		"Listing all %d racks, %s",
		memoData.RackCount,
		memoData.RackSizeSummary.String())

	b := common.URLPrefix(r)

	msg := ""
	nl := ""

	err = WS.Inventory.ScanRacksInZone(
		ctx,
		inventory.DefaultRegion,
		inventory.DefaultZone,
		func(name string) error {
			target := fmt.Sprintf("%s%s/", b, name)

			racks = append(racks, &pb.External_RackSummary{
				Uri:  target,
				Name: name,
			})

			msg = fmt.Sprintf("%s%sListing rack %q at %q", msg, nl, name, target)
			nl = "\n"

			return nil
		})

	sort.Slice(racks, func(i, j int) bool {
		return racks[i].Name < racks[j].Name
	})
	res.Racks = racks

	tracing.Info(ctx, msg)

	if err != nil {
		HTTP.PostError(ctx, w, err)
		return
	}

	err = marshalJson(w, res)
	HTTP.PostErrorIf(ctx, w, err)
}

func handlerRackRead(w http.ResponseWriter, r *http.Request) {
	ctx, span := tracing.StartSpan(context.Background(),
		tracing.WithName("Get rack Details"),
		tracing.WithContextValue(timestamp.EnsureTickInContext),
		tracing.AsInternal())
	defer span.End()

	vars := mux.Vars(r)
	rackID := vars["rackID"]

	err := doSessionHeader(
		ctx, w, r,
		func(_ context.Context, session *sessions.Session) error {
			return WS.sessions.ensureEstablishedSession(session)
		})

	if err != nil {
		HTTP.PostError(ctx, w, err)
		return
	}

	rd, ra, ro, err := WS.Inventory.GetRackInZone(
		ctx,
		inventory.DefaultRegion,
		inventory.DefaultZone,
		rackID)

	if err != nil {
		HTTP.PostError(ctx, w, err)
		return
	}

	rack, err := transformRack(ctx, rd, ra, ro)
	if err != nil {
		HTTP.PostError(ctx, w, err)
		return
	}

	w.Header().Set("Content-Type", "application/json")

	tracing.UpdateSpanName(ctx, "Get Details for rack %q", rackID)
	tracing.Info(ctx, "rack %q details: %v", rackID, rack)

	err = marshalJson(w, rack)

	HTTP.PostErrorIf(ctx, w, err)
}

func handlerBladesList(w http.ResponseWriter, r *http.Request) {
	ctx, span := tracing.StartSpan(context.Background(),
		tracing.WithName("Get List of Blades in Selected rack"),
		tracing.WithContextValue(timestamp.EnsureTickInContext),
		tracing.AsInternal())
	defer span.End()

	err := doSessionHeader(
		ctx, w, r,
		func(_ context.Context, session *sessions.Session) error {
			return WS.sessions.ensureEstablishedSession(session)
		})

	if err != nil {
		HTTP.PostError(ctx, w, err)
		return
	}

	vars := mux.Vars(r)
	rackID := vars["rackID"] 

	tracing.UpdateSpanName(ctx, "Get List of Blades in rack %q", rackID)

	if _, err = fmt.Fprintf(w, "Blades in %q (List)\n", rackID); err != nil {
		HTTP.PostError(ctx, w, err)
		return
	}

	b := common.URLPrefix(r)

	err = WS.Inventory.ScanBladesInRack(
		ctx,
		inventory.DefaultRegion,
		inventory.DefaultZone,
		rackID,
		func(bladeID int64) error {

			target := fmt.Sprintf("%s%d", b, bladeID)
			tracing.Info(ctx, " Listing blades '%d' at %q", bladeID, target)

			_, err = fmt.Fprintln(w, target)
			return err
		})

	HTTP.PostErrorIf(ctx, w, err)
}

func handlerBladeRead(w http.ResponseWriter, r *http.Request) {
	ctx, span := tracing.StartSpan(context.Background(),
		tracing.WithName("Get Blade Details"),
		tracing.WithContextValue(timestamp.EnsureTickInContext),
		tracing.AsInternal())
	defer span.End()

	err := doSessionHeader(
		ctx, w, r,
		func(_ context.Context, session *sessions.Session) error {
			return WS.sessions.ensureEstablishedSession(session)
		})

	if err != nil {
		HTTP.PostError(ctx, w, err)
		return
	}

	vars := mux.Vars(r)
	rackID := vars["rackID"]
	bladeName := vars["bladeID"]

	w.Header().Set("Content-Type", "application/json")

	bladeID, err := strconv.ParseInt(bladeName, 10, 64)
	if err != nil {
		HTTP.PostError(ctx, w, &HTTP.Error{
			SC:   http.StatusBadRequest,
			Base: err,
		})

		return
	}

	blade, err := WS.Inventory.GetBlade(
		ctx,
		inventory.DefaultRegion,
		inventory.DefaultZone,
		rackID,
		bladeID)
	if err != nil {
		HTTP.PostError(ctx, w, err)
		return
	}

	tracing.UpdateSpanName(ctx, "Get Details for blade %d in rack %q", bladeID, rackID)
	tracing.Info(ctx, "Returning details for blade %d in rack %q:  %v", bladeID, rackID, blade)

	err = marshalJson(w, blade)

	HTTP.PostErrorIf(ctx, w, err)
}

func transformRack(
	ctx context.Context,
	rd *pb.Definition_Rack,
	ra *pb.Actual_Rack,
	ro *pb.Observed_Rack) (*pb.External_Rack, error) {
	rack := &pb.External_Rack{
		Details: rd.GetDetails(),
	}

	blades := make([]*pb.External_Blade, 0, len(rd.Blades))

	for i, blade := range rd.Blades {
		b := &pb.External_Blade{
			Index:         i,
			Details:       blade.GetDetails(),
			Capacity:      blade.GetCapacity(),
			BootOnPowerOn: blade.BootOnPowerOn,
			BootInfo:      blade.GetBootInfo(),
		}

		if ba, ok := ra.Blades[i]; ok {
			usage := copyAndSortUsage(ba.GetUsage())

			b.Actual = &pb.External_Blade_ActualState{
				SmState:   ba.GetSmState(),
				EnteredAt: ba.GetCore().GetEnteredAt(),
				Usage:     usage,
				Avail:     ba.GetAvail(),
			}

			b.Observed = &pb.External_Blade_ObservedState{
				At:        common.TickFromContext(ctx),
				SmState:   ba.GetSmState(),
				EnteredAt: ba.GetCore().GetEnteredAt(),
				Usage:     usage,
				Avail:     ba.GetAvail(),
			}

			if bo, ok := ro.Blades[i]; ok && bo.Core.EnteredAt <= ba.Core.EnteredAt {
				usage := copyAndSortUsage(bo.GetUsage())

				b.Observed.At = bo.GetObservedAt()
				b.Observed.SmState = bo.GetSmState()
				b.Observed.EnteredAt = bo.GetCore().GetEnteredAt()
				b.Observed.Usage = usage
				b.Observed.Avail = bo.GetAvail()
			}
		}

		blades = append(blades, b)
	}

	sort.Slice(blades, func(i, j int) bool { return blades[i].Index < blades[j].Index })
	rack.FullBlades = blades

	tors := make([]*pb.External_Tor, 0, len(rd.Tors))

	for i, tor := range rd.Tors {
		t := &pb.External_Tor{
			Index:   i,
			Details: tor.GetDetails(),
		}

		ports := make([]*pb.External_Tor_Port, 0, len(rd.Tors))

		for k, port := range tor.GetPorts() {
			ports = append(ports, &pb.External_Tor_Port{
				Id:   k,
				Port: port,
			})
		}
		sort.Slice(ports, func(i, j int) bool {
			return ports[i].GetId() < ports[j].GetId()
		})

		t.Ports = ports

		if ta, ok := ra.Tors[i]; ok {
			cables := make([]*pb.Cable, 0, len(ta.Cables))
			for _, cable := range ta.GetCables() {
				cables = append(cables, cable)
			}

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

			t.Actual = &pb.External_Tor_ActualState{
				SmState:   ta.GetSmState(),
				EnteredAt: ta.GetCore().GetEnteredAt(),
				Cables:    cables,
			}

			t.Observed = &pb.External_Tor_ObservedState{
				At:        common.TickFromContext(ctx),
				SmState:   ta.GetSmState(),
				EnteredAt: ta.GetCore().GetEnteredAt(),
				Cables:    cables,
			}
		}

		tors = append(tors, t)
	}

	sort.Slice(tors, func(i, j int) bool { return tors[i].Index < tors[j].Index })
	rack.Tors = tors

	pdus := make([]*pb.External_Pdu, 0, len(rd.Pdus))

	for i, pdu := range rd.Pdus {
		p := &pb.External_Pdu{
			Index:   i,
			Details: pdu.GetDetails(),
		}

		ports := make([]*pb.External_Pdu_Port, 0, len(rd.Pdus))

		for k, port := range pdu.GetPorts() {
			ports = append(ports, &pb.External_Pdu_Port{
				Id:   k,
				Port: port,
			})
		}
		sort.Slice(ports, func(i, j int) bool {
			return ports[i].GetId() < ports[j].GetId()
		})

		p.Ports = ports

		if pa, ok := ra.Pdus[i]; ok {
			cables := make([]*pb.Cable, 0, len(pa.Cables))
			for _, cable := range pa.GetCables() {
				cables = append(cables, cable)
			}

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

			p.Actual = &pb.External_Pdu_ActualState{
				SmState:   pa.GetSmState(),
				EnteredAt: pa.GetCore().GetEnteredAt(),
				Cables:    cables,
			}

			p.Observed = &pb.External_Pdu_ObservedState{
				At:        common.TickFromContext(ctx),
				SmState:   pa.GetSmState(),
				EnteredAt: pa.GetCore().GetEnteredAt(),
				Cables:    cables,
			}
		}

		pdus = append(pdus, p)
	}

	sort.Slice(pdus, func(i, j int) bool { return pdus[i].Index < pdus[j].Index })
	rack.Pdus = pdus

	return rack, nil
}

func copyAndSortUsage(original map[string]*pb.Usage) []*pb.Usage {
	usage := make([]*pb.Usage, 0, len(original))

	for _, u := range original {
		usage = append(usage, u)
	}
	sort.Slice(usage, func(i, j int) bool { return usage[i].InstanceId < usage[j].InstanceId })
	return usage
}
// This module contains the routines, etc. to implement the frontend handlers
// for the inventory operators part of the API.
//

package frontend

import (
	"context"
	"fmt"
	"net/http"
	"sort"
	"strconv"

	"github.com/Jim3Things/CloudChamber/simulation/pkg/errors/HTTP"
	"github.com/gorilla/sessions"

	"github.com/gorilla/mux"

	"github.com/Jim3Things/CloudChamber/simulation/internal/clients/inventory"
	"github.com/Jim3Things/CloudChamber/simulation/internal/clients/timestamp"
	"github.com/Jim3Things/CloudChamber/simulation/internal/common"
	"github.com/Jim3Things/CloudChamber/simulation/internal/tracing"
	"github.com/Jim3Things/CloudChamber/simulation/pkg/errors"
	pb "github.com/Jim3Things/CloudChamber/simulation/pkg/protos/inventory"
)

func inventoryAddRoutes(routeBase *mux.Router) {

	routeRacks := routeBase.PathPrefix("/racks").Subrouter()

	routeRacks.HandleFunc("", handlerRacksList).Methods("GET")
	routeRacks.HandleFunc("/", handlerRacksList).Methods("GET")

	// In the following, the "GET" method is allowed just for the purposes of test and
	// evaluation. At some point, it will need to be removed, but in the meantime, leaving
	// it there allows simple experimentation with just a browser.
	//
	// As a reminder,
	//	 PUT is idempotent so translates to UPDATE in the CRUD methodology
	//   POST is NOT idempotent so translates to CREATE in the CRUD methodology
	//
	// routeRacks.HandleFunc("/racks/{rackID}", handlerRacksCreate).Methods("POST", "GET") // May be only GET
	routeRacks.HandleFunc("/{rackID}", handlerRackRead).Methods("GET")
	routeRacks.HandleFunc("/{rackID}/", handlerRackRead).Methods("GET")
	routeRacks.HandleFunc("/{rackID}/blades", handlerBladesList).Methods("GET")
	routeRacks.HandleFunc("/{rackID}/blades/", handlerBladesList).Methods("GET")
	routeRacks.HandleFunc("/{rackID}/blades/{bladeID}", handlerBladeRead).Methods("GET")
	// routeRacks.HandleFunc("/racks/{rackID}", handlerRacksDelete).Methods("DELETE", "GET")
}

func handlerRacksList(w http.ResponseWriter, r *http.Request) {
	ctx, span := tracing.StartSpan(context.Background(),
		tracing.WithName("Get Cluster Inventory List of Racks"),
		tracing.WithContextValue(timestamp.EnsureTickInContext),
		tracing.AsInternal())
	defer span.End()

	err := doSessionHeader(
		ctx, w, r,
		func(_ context.Context, session *sessions.Session) error {
			return WS.sessions.ensureEstablishedSession(session)
		})

	if err != nil {
		HTTP.PostError(ctx, w, err)
		return
	}

	w.Header().Set("Content-Type", "application/json")

	memoData := WS.Inventory.GetMemoData()
	if memoData == nil {
		HTTP.PostError(ctx, w, errors.ErrInventoryNotAvailable)
		return
	}

	zoneDetails, err := WS.Inventory.GetZoneDetails(ctx, inventory.DefaultRegion, inventory.DefaultZone)
	if err != nil {
		HTTP.PostError(ctx, w, errors.ErrInventoryNotAvailable)
		return
	}

	racks := make([]*pb.External_RackSummary, 0, memoData.RackCount)

	res := &pb.External_ZoneSummary{
		MaxBladeCount: int64(memoData.MaxBladeCount),
		MaxCapacity:   memoData.MaxCapacity,
		MaxTorCount:   int64(memoData.MaxTorCount),
		MaxPduCount:   int64(memoData.MaxPduCount),
		MaxConnectors: int64(memoData.MaxConnectors),
		Name:          inventory.DefaultZone,
		Details:       zoneDetails,
	}

	tracing.Info(
		ctx,
		"Listing all %d racks, %s",
		memoData.RackCount,
		memoData.RackSizeSummary.String())

	b := common.URLPrefix(r)

	// We're going to group all racks listed together into a single trace entry.
	//
	// msg is the formatted text built so far.
	// nl is the connector between racks.  It starts as nothing and is changed
	// to a new-line once anything has been stored into the formatted text.
	msg := ""
	nl := ""

	err = WS.Inventory.ScanRacksInZone(
		ctx,
		inventory.DefaultRegion,
		inventory.DefaultZone,
		func(name string) error {
			target := fmt.Sprintf("%s%s/", b, name)

			racks = append(racks, &pb.External_RackSummary{
				Uri:  target,
				Name: name,
			})

			msg = fmt.Sprintf("%s%sListing rack %q at %q", msg, nl, name, target)
			nl = "\n"

			return nil
		})

	sort.Slice(racks, func(i, j int) bool {
		return racks[i].Name < racks[j].Name
	})
	res.Racks = racks

	tracing.Info(ctx, msg)

	if err != nil {
		HTTP.PostError(ctx, w, err)
		return
	}

	err = marshalJson(w, res)
	HTTP.PostErrorIf(ctx, w, err)
}

func handlerRackRead(w http.ResponseWriter, r *http.Request) {
	ctx, span := tracing.StartSpan(context.Background(),
		tracing.WithName("Get rack Details"),
		tracing.WithContextValue(timestamp.EnsureTickInContext),
		tracing.AsInternal())
	defer span.End()

	vars := mux.Vars(r)
	rackID := vars["rackID"]

	err := doSessionHeader(
		ctx, w, r,
		func(_ context.Context, session *sessions.Session) error {
			return WS.sessions.ensureEstablishedSession(session)
		})

	if err != nil {
		HTTP.PostError(ctx, w, err)
		return
	}

	rd, ra, ro, err := WS.Inventory.GetRackInZone(
		ctx,
		inventory.DefaultRegion,
		inventory.DefaultZone,
		rackID)

	if err != nil {
		HTTP.PostError(ctx, w, err)
		return
	}

	rack, err := transformRack(ctx, rd, ra, ro)
	if err != nil {
		HTTP.PostError(ctx, w, err)
		return
	}

	w.Header().Set("Content-Type", "application/json")

	tracing.UpdateSpanName(ctx, "Get Details for rack %q", rackID)
	tracing.Info(ctx, "rack %q details: %v", rackID, rack)

	err = marshalJson(w, rack)

	HTTP.PostErrorIf(ctx, w, err)
}

func handlerBladesList(w http.ResponseWriter, r *http.Request) {
	ctx, span := tracing.StartSpan(context.Background(),
		tracing.WithName("Get List of Blades in Selected rack"),
		tracing.WithContextValue(timestamp.EnsureTickInContext),
		tracing.AsInternal())
	defer span.End()

	err := doSessionHeader(
		ctx, w, r,
		func(_ context.Context, session *sessions.Session) error {
			return WS.sessions.ensureEstablishedSession(session)
		})

	if err != nil {
		HTTP.PostError(ctx, w, err)
		return
	}

	vars := mux.Vars(r)
	rackID := vars["rackID"] // captured the key value in rackID variable

	tracing.UpdateSpanName(ctx, "Get List of Blades in rack %q", rackID)

	if _, err = fmt.Fprintf(w, "Blades in %q (List)\n", rackID); err != nil {
		HTTP.PostError(ctx, w, err)
		return
	}

	b := common.URLPrefix(r)

	err = WS.Inventory.ScanBladesInRack(
		ctx,
		inventory.DefaultRegion,
		inventory.DefaultZone,
		rackID,
		func(bladeID int64) error {

			target := fmt.Sprintf("%s%d", b, bladeID)
			tracing.Info(ctx, " Listing blades '%d' at %q", bladeID, target)

			_, err = fmt.Fprintln(w, target)
			return err
		})

	HTTP.PostErrorIf(ctx, w, err)
}

func handlerBladeRead(w http.ResponseWriter, r *http.Request) {
	ctx, span := tracing.StartSpan(context.Background(),
		tracing.WithName("Get Blade Details"),
		tracing.WithContextValue(timestamp.EnsureTickInContext),
		tracing.AsInternal())
	defer span.End()

	err := doSessionHeader(
		ctx, w, r,
		func(_ context.Context, session *sessions.Session) error {
			return WS.sessions.ensureEstablishedSession(session)
		})

	if err != nil {
		HTTP.PostError(ctx, w, err)
		return
	}

	vars := mux.Vars(r)
	rackID := vars["rackID"]
	bladeName := vars["bladeID"]

	w.Header().Set("Content-Type", "application/json")

	bladeID, err := strconv.ParseInt(bladeName, 10, 64)
	if err != nil {
		HTTP.PostError(ctx, w, &HTTP.Error{
			SC:   http.StatusBadRequest,
			Base: err,
		})

		return
	}

	blade, err := WS.Inventory.GetBlade(
		ctx,
		inventory.DefaultRegion,
		inventory.DefaultZone,
		rackID,
		bladeID)
	if err != nil {
		HTTP.PostError(ctx, w, err)
		return
	}

	tracing.UpdateSpanName(ctx, "Get Details for blade %d in rack %q", bladeID, rackID)
	tracing.Info(ctx, "Returning details for blade %d in rack %q:  %v", bladeID, rackID, blade)

	err = marshalJson(w, blade)

	HTTP.PostErrorIf(ctx, w, err)
}

// transformRack converts the internal rack states into an exportable external
// view.
func transformRack(
	ctx context.Context,
	rd *pb.Definition_Rack,
	ra *pb.Actual_Rack,
	ro *pb.Observed_Rack) (*pb.External_Rack, error) {
	rack := &pb.External_Rack{
		Details: rd.GetDetails(),
	}

	blades := make([]*pb.External_Blade, 0, len(rd.Blades))

	for i, blade := range rd.Blades {
		b := &pb.External_Blade{
			Index:         i,
			Details:       blade.GetDetails(),
			Capacity:      blade.GetCapacity(),
			BootOnPowerOn: blade.BootOnPowerOn,
			BootInfo:      blade.GetBootInfo(),
		}

		if ba, ok := ra.Blades[i]; ok {
			usage := copyAndSortUsage(ba.GetUsage())

			b.Actual = &pb.External_Blade_ActualState{
				SmState:   ba.GetSmState(),
				EnteredAt: ba.GetCore().GetEnteredAt(),
				Usage:     usage,
				Avail:     ba.GetAvail(),
			}

			b.Observed = &pb.External_Blade_ObservedState{
				At:        common.TickFromContext(ctx),
				SmState:   ba.GetSmState(),
				EnteredAt: ba.GetCore().GetEnteredAt(),
				Usage:     usage,
				Avail:     ba.GetAvail(),
			}

			// if there is an observed state, and that state is from no later
			// than the actual state, copy that to the external display.
			//
			// Note that the observed state may represent a later state if that
			// state is from an earlier run, and the observed state has not yet
			// been reset.
			if bo, ok := ro.Blades[i]; ok && bo.Core.EnteredAt <= ba.Core.EnteredAt {
				usage := copyAndSortUsage(bo.GetUsage())

				b.Observed.At = bo.GetObservedAt()
				b.Observed.SmState = bo.GetSmState()
				b.Observed.EnteredAt = bo.GetCore().GetEnteredAt()
				b.Observed.Usage = usage
				b.Observed.Avail = bo.GetAvail()
			}
		}

		blades = append(blades, b)
	}

	sort.Slice(blades, func(i, j int) bool { return blades[i].Index < blades[j].Index })
	rack.FullBlades = blades

	tors := make([]*pb.External_Tor, 0, len(rd.Tors))

	for i, tor := range rd.Tors {
		t := &pb.External_Tor{
			Index:   i,
			Details: tor.GetDetails(),
		}

		ports := make([]*pb.External_Tor_Port, 0, len(rd.Tors))

		for k, port := range tor.GetPorts() {
			ports = append(ports, &pb.External_Tor_Port{
				Id:   k,
				Port: port,
			})
		}
		sort.Slice(ports, func(i, j int) bool {
			return ports[i].GetId() < ports[j].GetId()
		})

		t.Ports = ports

		if ta, ok := ra.Tors[i]; ok {
			cables := make([]*pb.Cable, 0, len(ta.Cables))
			for _, cable := range ta.GetCables() {
				cables = append(cables, cable)
			}

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

			t.Actual = &pb.External_Tor_ActualState{
				SmState:   ta.GetSmState(),
				EnteredAt: ta.GetCore().GetEnteredAt(),
				Cables:    cables,
			}

			t.Observed = &pb.External_Tor_ObservedState{
				At:        common.TickFromContext(ctx),
				SmState:   ta.GetSmState(),
				EnteredAt: ta.GetCore().GetEnteredAt(),
				Cables:    cables,
			}
		}

		tors = append(tors, t)
	}

	sort.Slice(tors, func(i, j int) bool { return tors[i].Index < tors[j].Index })
	rack.Tors = tors

	pdus := make([]*pb.External_Pdu, 0, len(rd.Pdus))

	for i, pdu := range rd.Pdus {
		p := &pb.External_Pdu{
			Index:   i,
			Details: pdu.GetDetails(),
		}

		ports := make([]*pb.External_Pdu_Port, 0, len(rd.Pdus))

		for k, port := range pdu.GetPorts() {
			ports = append(ports, &pb.External_Pdu_Port{
				Id:   k,
				Port: port,
			})
		}
		sort.Slice(ports, func(i, j int) bool {
			return ports[i].GetId() < ports[j].GetId()
		})

		p.Ports = ports

		if pa, ok := ra.Pdus[i]; ok {
			cables := make([]*pb.Cable, 0, len(pa.Cables))
			for _, cable := range pa.GetCables() {
				cables = append(cables, cable)
			}

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

			p.Actual = &pb.External_Pdu_ActualState{
				SmState:   pa.GetSmState(),
				EnteredAt: pa.GetCore().GetEnteredAt(),
				Cables:    cables,
			}

			p.Observed = &pb.External_Pdu_ObservedState{
				At:        common.TickFromContext(ctx),
				SmState:   pa.GetSmState(),
				EnteredAt: pa.GetCore().GetEnteredAt(),
				Cables:    cables,
			}
		}

		pdus = append(pdus, p)
	}

	sort.Slice(pdus, func(i, j int) bool { return pdus[i].Index < pdus[j].Index })
	rack.Pdus = pdus

	return rack, nil
}

func copyAndSortUsage(original map[string]*pb.Usage) []*pb.Usage {
	usage := make([]*pb.Usage, 0, len(original))

	for _, u := range original {
		usage = append(usage, u)
	}
	sort.Slice(usage, func(i, j int) bool { return usage[i].InstanceId < usage[j].InstanceId })
	return usage
}
