package libnetwork

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"

	"github.com/containerd/log"
	"github.com/docker/docker/libnetwork/datastore"
	"github.com/docker/docker/libnetwork/osl"
	"github.com/docker/docker/libnetwork/scope"
	"github.com/docker/docker/pkg/stringid"
)

const (
	sandboxPrefix = "sandbox"
)

type epState struct {
	Eid string
	Nid string
}

type sbState struct {
	ID         string
	Cid        string
	c          *Controller
	dbIndex    uint64
	dbExists   bool
	Eps        []epState
	EpPriority map[string]int
	// external servers have to be persisted so that on restart of a live-restore
	// enabled daemon we get the external servers for the running containers.
	//
	// It is persisted as "ExtDNS2" for historical reasons. ExtDNS2 was used to
	// handle migration between docker < 1.14 and >= 1.14. Before version 1.14 we
	// used ExtDNS but with a []string. As it's unlikely that installations still
	// have state from before 1.14, we've dropped the migration code.
	ExtDNS []extDNSEntry `json:"ExtDNS2"`
}

func (sbs *sbState) Key() []string {
	return []string{sandboxPrefix, sbs.ID}
}

func (sbs *sbState) KeyPrefix() []string {
	return []string{sandboxPrefix}
}

func (sbs *sbState) Value() []byte {
	b, err := json.Marshal(sbs)
	if err != nil {
		return nil
	}
	return b
}

func (sbs *sbState) SetValue(value []byte) error {
	return json.Unmarshal(value, sbs)
}

func (sbs *sbState) Index() uint64 {
	sb, err := sbs.c.SandboxByID(sbs.ID)
	if err != nil {
		return sbs.dbIndex
	}

	maxIndex := sb.dbIndex
	if sbs.dbIndex > maxIndex {
		maxIndex = sbs.dbIndex
	}

	return maxIndex
}

func (sbs *sbState) SetIndex(index uint64) {
	sbs.dbIndex = index
	sbs.dbExists = true

	sb, err := sbs.c.SandboxByID(sbs.ID)
	if err != nil {
		return
	}

	sb.dbIndex = index
	sb.dbExists = true
}

func (sbs *sbState) Exists() bool {
	if sbs.dbExists {
		return sbs.dbExists
	}

	sb, err := sbs.c.SandboxByID(sbs.ID)
	if err != nil {
		return false
	}

	return sb.dbExists
}

func (sbs *sbState) Skip() bool {
	return false
}

func (sbs *sbState) New() datastore.KVObject {
	return &sbState{c: sbs.c}
}

func (sbs *sbState) CopyTo(o datastore.KVObject) error {
	dstSbs := o.(*sbState)
	dstSbs.c = sbs.c
	dstSbs.ID = sbs.ID
	dstSbs.Cid = sbs.Cid
	dstSbs.dbIndex = sbs.dbIndex
	dstSbs.dbExists = sbs.dbExists
	dstSbs.EpPriority = sbs.EpPriority

	dstSbs.Eps = append(dstSbs.Eps, sbs.Eps...)
	dstSbs.ExtDNS = append(dstSbs.ExtDNS, sbs.ExtDNS...)

	return nil
}

func (sb *Sandbox) storeUpdate(ctx context.Context) error {
	sbs := &sbState{
		c:          sb.controller,
		ID:         sb.id,
		Cid:        sb.containerID,
		EpPriority: sb.epPriority,
		ExtDNS:     sb.extDNS,
	}

retry:
	sbs.Eps = nil
	for _, ep := range sb.Endpoints() {
		// If the endpoint is not persisted then do not add it to
		// the sandbox checkpoint
		if ep.Skip() {
			continue
		}

		sbs.Eps = append(sbs.Eps, epState{
			Nid: ep.getNetwork().ID(),
			Eid: ep.ID(),
		})
	}

	err := sb.controller.updateToStore(ctx, sbs)
	if errors.Is(err, datastore.ErrKeyModified) {
		// When we get ErrKeyModified it is sufficient to just
		// go back and retry.  No need to get the object from
		// the store because we always regenerate the store
		// state from in memory sandbox state
		goto retry
	}

	return err
}

func (sb *Sandbox) storeDelete() error {
	return sb.controller.store.DeleteObject(&sbState{
		c:        sb.controller,
		ID:       sb.id,
		Cid:      sb.containerID,
		dbExists: sb.dbExists,
	})
}

// sandboxRestore restores Sandbox objects from the store, deleting them if they're not active.
func (c *Controller) sandboxRestore(activeSandboxes map[string]interface{}) error {
	sandboxStates, err := c.store.List(&sbState{c: c})
	if err != nil {
		if errors.Is(err, datastore.ErrKeyNotFound) {
			// It's normal for no sandboxes to be found. Just bail out.
			return nil
		}
		return fmt.Errorf("failed to get sandboxes: %v", err)
	}

	for _, s := range sandboxStates {
		sbs := s.(*sbState)
		sb := &Sandbox{
			id:                 sbs.ID,
			controller:         sbs.c,
			containerID:        sbs.Cid,
			epPriority:         sbs.EpPriority,
			extDNS:             sbs.ExtDNS,
			endpoints:          []*Endpoint{},
			populatedEndpoints: map[string]struct{}{},
			dbIndex:            sbs.dbIndex,
			isStub:             true,
			dbExists:           true,
		}

		create := true
		isRestore := false
		if val, ok := activeSandboxes[sb.ID()]; ok {
			sb.isStub = false
			isRestore = true
			opts := val.([]SandboxOption)
			sb.processOptions(opts...)
			sb.restoreHostsPath()
			sb.restoreResolvConfPath()
			create = !sb.config.useDefaultSandBox
		}

		ctx := context.TODO()
		ctx = log.WithLogger(ctx, log.G(ctx).WithFields(log.Fields{
			"sid":       stringid.TruncateID(sb.ID()),
			"cid":       stringid.TruncateID(sb.ContainerID()),
			"isRestore": isRestore,
		}))

		sb.osSbox, err = osl.NewSandbox(sb.Key(), create, isRestore)
		if err != nil {
			log.G(ctx).WithError(err).Error("Failed to create osl sandbox while trying to restore sandbox")
			continue
		}

		c.mu.Lock()
		c.sandboxes[sb.id] = sb
		c.mu.Unlock()

		for _, eps := range sbs.Eps {
			ctx := log.WithLogger(ctx, log.G(ctx).WithFields(log.Fields{
				"nid": stringid.TruncateID(eps.Nid),
				"eid": stringid.TruncateID(eps.Eid),
			}))
			// If the Network or Endpoint can't be loaded from the store, log and continue. Something
			// might go wrong later, but it might just be a reference to a deleted network/endpoint
			// (in which case, the best thing to do is to continue to run/delete the Sandbox with the
			// available configuration).
			n, err := c.getNetworkFromStore(eps.Nid)
			if err != nil {
				log.G(ctx).WithError(err).Warn("Failed to restore endpoint, getNetworkFromStore failed")
				continue
			}
			ep, err := n.getEndpointFromStore(eps.Eid)
			if err != nil {
				log.G(ctx).WithError(err).Warn("Failed to restore endpoint, getEndpointFromStore failed")
				continue
			}
			sb.addEndpoint(ep)
		}

		if !isRestore {
			log.G(ctx).Info("Removing stale sandbox")
			if err := sb.delete(context.WithoutCancel(ctx), true); err != nil {
				log.G(ctx).WithError(err).Warn("Failed to delete sandbox while trying to clean up")
			}
			continue
		}

		for _, ep := range sb.endpoints {
			sb.populatedEndpoints[ep.id] = struct{}{}
		}

		// reconstruct osl sandbox field
		if !sb.config.useDefaultSandBox {
			if err := sb.restoreOslSandbox(); err != nil {
				log.G(ctx).WithError(err).Error("Failed to populate fields for osl sandbox")
				continue
			}
		} else {
			// FIXME(thaJeztah): osSbox (and thus defOsSbox) is always nil on non-Linux: move this code to Linux-only files.
			c.defOsSboxOnce.Do(func() {
				c.defOsSbox = sb.osSbox
			})
		}

		for _, ep := range sb.endpoints {
			if !c.isAgent() {
				n := ep.getNetwork()
				if !c.isSwarmNode() || n.Scope() != scope.Swarm || !n.driverIsMultihost() {
					n.updateSvcRecord(context.WithoutCancel(ctx), ep, true)
				}
			}
		}
	}

	return nil
}
