package llbsolver

import (
	"context"
	"fmt"
	"slices"
	"strings"

	"github.com/containerd/platforms"
	"github.com/moby/buildkit/solver"
	"github.com/moby/buildkit/solver/llbsolver/cdidevices"
	"github.com/moby/buildkit/solver/llbsolver/ops/opsutils"
	"github.com/moby/buildkit/solver/pb"
	"github.com/moby/buildkit/util/apicaps"
	"github.com/moby/buildkit/util/entitlements"
	digest "github.com/opencontainers/go-digest"
	ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
	"github.com/pkg/errors"
)

type vertex struct {
	sys     any
	options solver.VertexOptions
	inputs  []solver.Edge
	digest  digest.Digest
	name    string
}

func (v *vertex) Digest() digest.Digest {
	return v.digest
}

func (v *vertex) Sys() any {
	return v.sys
}

func (v *vertex) Options() solver.VertexOptions {
	return v.options
}

func (v *vertex) Inputs() []solver.Edge {
	return v.inputs
}

func (v *vertex) Name() string {
	if name, ok := v.options.Description["llb.customname"]; ok {
		return name
	}
	return v.name
}

type LoadOpt func(*pb.Op, *pb.OpMetadata, *solver.VertexOptions) error

func WithValidateCaps() LoadOpt {
	cs := pb.Caps.CapSet(pb.Caps.All())
	return func(_ *pb.Op, md *pb.OpMetadata, opt *solver.VertexOptions) error {
		if md != nil {
			for c := range md.Caps {
				if err := cs.Supports(apicaps.CapID(c)); err != nil {
					return err
				}
			}
		}
		return nil
	}
}

func WithCacheSources(cms []solver.CacheManager) LoadOpt {
	return func(_ *pb.Op, _ *pb.OpMetadata, opt *solver.VertexOptions) error {
		opt.CacheSources = cms
		return nil
	}
}

func NormalizeRuntimePlatforms() LoadOpt {
	var defaultPlatform *pb.Platform
	return func(op *pb.Op, _ *pb.OpMetadata, opt *solver.VertexOptions) error {
		if op.Platform == nil {
			if defaultPlatform == nil {
				p := platforms.DefaultSpec()
				defaultPlatform = &pb.Platform{
					OS:           p.OS,
					Architecture: p.Architecture,
					Variant:      p.Variant,
					OSVersion:    p.OSVersion,
					OSFeatures:   p.OSFeatures,
				}
			}
			op.Platform = defaultPlatform
		}
		platform := ocispecs.Platform{
			OS:           op.Platform.OS,
			Architecture: op.Platform.Architecture,
			Variant:      op.Platform.Variant,
			OSVersion:    op.Platform.OSVersion,
			OSFeatures:   op.Platform.OSFeatures,
		}
		normalizedPlatform := platforms.Normalize(platform)

		op.Platform = &pb.Platform{
			OS:           normalizedPlatform.OS,
			Architecture: normalizedPlatform.Architecture,
			Variant:      normalizedPlatform.Variant,
			OSVersion:    normalizedPlatform.OSVersion,
		}
		if normalizedPlatform.OSFeatures != nil {
			op.Platform.OSFeatures = slices.Clone(normalizedPlatform.OSFeatures)
		}

		return nil
	}
}

func ValidateEntitlements(ent entitlements.Set, cdiManager *cdidevices.Manager) LoadOpt {
	return func(op *pb.Op, _ *pb.OpMetadata, opt *solver.VertexOptions) error {
		switch op := op.Op.(type) {
		case *pb.Op_Exec:
			v := entitlements.Values{
				NetworkHost:      op.Exec.Network == pb.NetMode_HOST,
				SecurityInsecure: op.Exec.Security == pb.SecurityMode_INSECURE,
			}
			if err := ent.Check(v); err != nil {
				return err
			}
			if device := op.Exec.CdiDevices; len(device) > 0 {
				var cfg *entitlements.DevicesConfig
				if ent, ok := ent[entitlements.EntitlementDevice]; ok {
					cfg, ok = ent.(*entitlements.DevicesConfig)
					if !ok {
						return errors.Errorf("invalid device entitlement config %T", ent)
					}
				}
				if cfg != nil && cfg.All {
					return nil
				}

				var allowedDevices []*pb.CDIDevice
				var nonAliasedDevices []*pb.CDIDevice
				for _, d := range cdiManager.ListDevices() {
					if d.OnDemand && d.AutoAllow {
						allowedDevices = append(allowedDevices, &pb.CDIDevice{Name: d.Name})
					}
				}
				if cfg != nil {
					for _, d := range op.Exec.CdiDevices {
						if newName, ok := cfg.Devices[d.Name]; ok && newName != "" {
							d.Name = newName
							allowedDevices = append(allowedDevices, d)
						} else {
							nonAliasedDevices = append(nonAliasedDevices, d)
						}
					}
				} else {
					nonAliasedDevices = op.Exec.CdiDevices
				}

				mountedDevices, err := cdiManager.FindDevices(nonAliasedDevices...)
				if err != nil {
					return err
				}
				if len(mountedDevices) == 0 {
					op.Exec.CdiDevices = allowedDevices
					return nil
				}

				grantedDevices := make(map[string]struct{})
				if cfg != nil {
					for d := range cfg.Devices {
						resolved, err := cdiManager.FindDevices(&pb.CDIDevice{Name: d})
						if err != nil {
							return err
						}
						for _, r := range resolved {
							grantedDevices[r] = struct{}{}
						}
					}
				}

				var forbidden []string
				for _, d := range mountedDevices {
					if _, ok := grantedDevices[d]; !ok {
						if dev := cdiManager.GetDevice(d); !dev.AutoAllow {
							forbidden = append(forbidden, d)
							continue
						}
					}
					allowedDevices = append(allowedDevices, &pb.CDIDevice{Name: d})
				}

				if len(forbidden) > 0 {
					if len(forbidden) == 1 {
						return errors.Errorf("device %s is requested by the build but not allowed", forbidden[0])
					}
					return errors.Errorf("devices %s are requested by the build but not allowed", strings.Join(forbidden, ", "))
				}

				op.Exec.CdiDevices = allowedDevices
			}
		}
		return nil
	}
}

type detectPrunedCacheID struct {
	ids map[string]bool
}

func (dpc *detectPrunedCacheID) Load(op *pb.Op, md *pb.OpMetadata, opt *solver.VertexOptions) error {
	if md == nil || !md.IgnoreCache {
		return nil
	}
	switch op := op.Op.(type) {
	case *pb.Op_Exec:
		for _, m := range op.Exec.GetMounts() {
			if m.MountType == pb.MountType_CACHE {
				if m.CacheOpt != nil {
					id := m.CacheOpt.ID
					if id == "" {
						id = m.Dest
					}
					if dpc.ids == nil {
						dpc.ids = map[string]bool{}
					}
					// value shows in mount is on top of a ref
					dpc.ids[id] = m.Input != -1
				}
			}
		}
	}
	return nil
}

func Load(ctx context.Context, def *pb.Definition, polEngine SourcePolicyEvaluator, opts ...LoadOpt) (solver.Edge, error) {
	return loadLLB(ctx, def, polEngine, func(dgst digest.Digest, op *op, load func(digest.Digest) (solver.Vertex, error)) (solver.Vertex, error) {
		vtx, err := newVertex(dgst, op.Op, op.Metadata, load, opts...)
		if err != nil {
			return nil, err
		}
		return vtx, nil
	})
}

func newVertex(dgst digest.Digest, op *pb.Op, opMeta *pb.OpMetadata, load func(digest.Digest) (solver.Vertex, error), opts ...LoadOpt) (*vertex, error) {
	opt := solver.VertexOptions{}
	if opMeta != nil {
		opt.IgnoreCache = opMeta.IgnoreCache
		opt.Description = opMeta.Description
		if opMeta.ExportCache != nil {
			opt.ExportCache = &opMeta.ExportCache.Value
		}
		opt.ProgressGroup = opMeta.ProgressGroup
	}
	for _, fn := range opts {
		if err := fn(op, opMeta, &opt); err != nil {
			return nil, err
		}
	}

	name, err := llbOpName(op, func(dgst string) (solver.Vertex, error) {
		return load(digest.Digest(dgst))
	})
	if err != nil {
		return nil, err
	}
	vtx := &vertex{sys: op, options: opt, digest: dgst, name: name}
	for _, in := range op.Inputs {
		sub, err := load(digest.Digest(in.Digest))
		if err != nil {
			return nil, err
		}
		vtx.inputs = append(vtx.inputs, solver.Edge{Index: solver.Index(in.Index), Vertex: sub})
	}
	return vtx, nil
}

func recomputeDigests(ctx context.Context, all map[digest.Digest]*op, visited map[digest.Digest]digest.Digest, dgst digest.Digest) (digest.Digest, error) {
	if dgst, ok := visited[dgst]; ok {
		return dgst, nil
	}
	op, ok := all[dgst]
	if !ok {
		return "", errors.Errorf("invalid missing input digest %s", dgst)
	}

	for _, input := range op.Inputs {
		select {
		case <-ctx.Done():
			return "", context.Cause(ctx)
		default:
		}

		iDgst, err := recomputeDigests(ctx, all, visited, digest.Digest(input.Digest))
		if err != nil {
			return "", err
		}
		input.Digest = string(iDgst)
	}

	dt, err := op.Marshal()
	if err != nil {
		return "", err
	}

	newDgst := digest.FromBytes(dt)
	if newDgst != dgst {
		all[newDgst] = op
		delete(all, dgst)
	}
	visited[dgst] = newDgst
	return newDgst, nil
}

// op is a private wrapper around pb.Op that includes its metadata.
type op struct {
	*pb.Op
	Metadata *pb.OpMetadata
}

// loadLLB loads LLB.
// fn is executed sequentially.
func loadLLB(ctx context.Context, def *pb.Definition, polEngine SourcePolicyEvaluator, fn func(digest.Digest, *op, func(digest.Digest) (solver.Vertex, error)) (solver.Vertex, error)) (solver.Edge, error) {
	if len(def.Def) == 0 {
		return solver.Edge{}, errors.New("invalid empty definition")
	}

	allOps := make(map[digest.Digest]*op)

	var lastDgst digest.Digest

	for _, dt := range def.Def {
		var pbop pb.Op
		if err := pbop.Unmarshal(dt); err != nil {
			return solver.Edge{}, errors.Wrap(err, "failed to parse llb proto op")
		}
		dgst := digest.FromBytes(dt)
		if polEngine != nil {
			if _, err := polEngine.Evaluate(ctx, pbop.GetSource()); err != nil {
				return solver.Edge{}, errors.Wrap(err, "error evaluating the source policy")
			}
		}
		allOps[dgst] = &op{
			Op:       &pbop,
			Metadata: def.Metadata[string(dgst)],
		}
		lastDgst = dgst
	}

	mutatedDigests := make(map[digest.Digest]digest.Digest) // key: old, val: new
	for dgst := range allOps {
		if _, err := recomputeDigests(ctx, allOps, mutatedDigests, dgst); err != nil {
			return solver.Edge{}, err
		}
	}

	if len(allOps) < 2 {
		return solver.Edge{}, errors.Errorf("invalid LLB with %d vertexes", len(allOps))
	}

	for {
		newDgst, ok := mutatedDigests[lastDgst]
		if !ok || newDgst == lastDgst {
			break
		}
		lastDgst = newDgst
	}

	lastOp := allOps[lastDgst]
	delete(allOps, lastDgst)
	if len(lastOp.Inputs) == 0 {
		return solver.Edge{}, errors.Errorf("invalid LLB with no inputs on last vertex")
	}
	dgst := lastOp.Inputs[0].Digest

	cache := make(map[digest.Digest]solver.Vertex)

	var rec func(dgst digest.Digest) (solver.Vertex, error)
	rec = func(dgst digest.Digest) (solver.Vertex, error) {
		if v, ok := cache[dgst]; ok {
			return v, nil
		}
		op, ok := allOps[dgst]
		if !ok {
			return nil, errors.Errorf("invalid missing input digest %s", dgst)
		}

		if err := opsutils.Validate(op.Op); err != nil {
			return nil, err
		}

		v, err := fn(dgst, op, rec)
		if err != nil {
			return nil, err
		}
		cache[dgst] = v
		return v, nil
	}

	v, err := rec(digest.Digest(dgst))
	if err != nil {
		return solver.Edge{}, err
	}
	return solver.Edge{Vertex: v, Index: solver.Index(lastOp.Inputs[0].Index)}, nil
}

func llbOpName(pbOp *pb.Op, load func(string) (solver.Vertex, error)) (string, error) {
	switch op := pbOp.Op.(type) {
	case *pb.Op_Source:
		return op.Source.Identifier, nil
	case *pb.Op_Exec:
		return strings.Join(op.Exec.Meta.Args, " "), nil
	case *pb.Op_File:
		return fileOpName(op.File.Actions), nil
	case *pb.Op_Build:
		return "build", nil
	case *pb.Op_Merge:
		subnames := make([]string, len(pbOp.Inputs))
		for i, inp := range pbOp.Inputs {
			subvtx, err := load(inp.Digest)
			if err != nil {
				return "", err
			}
			subnames[i] = subvtx.Name()
		}
		return "merge " + fmt.Sprintf("(%s)", strings.Join(subnames, ", ")), nil
	case *pb.Op_Diff:
		var lowerName string
		if op.Diff.Lower.Input == -1 {
			lowerName = "scratch"
		} else {
			lowerVtx, err := load(pbOp.Inputs[op.Diff.Lower.Input].Digest)
			if err != nil {
				return "", err
			}
			lowerName = fmt.Sprintf("(%s)", lowerVtx.Name())
		}
		var upperName string
		if op.Diff.Upper.Input == -1 {
			upperName = "scratch"
		} else {
			upperVtx, err := load(pbOp.Inputs[op.Diff.Upper.Input].Digest)
			if err != nil {
				return "", err
			}
			upperName = fmt.Sprintf("(%s)", upperVtx.Name())
		}
		return "diff " + lowerName + " -> " + upperName, nil
	default:
		return "unknown", nil
	}
}

func fileOpName(actions []*pb.FileAction) string {
	names := make([]string, 0, len(actions))
	for _, action := range actions {
		switch a := action.Action.(type) {
		case *pb.FileAction_Mkdir:
			names = append(names, fmt.Sprintf("mkdir %s", a.Mkdir.Path))
		case *pb.FileAction_Mkfile:
			names = append(names, fmt.Sprintf("mkfile %s", a.Mkfile.Path))
		case *pb.FileAction_Symlink:
			names = append(names, fmt.Sprintf("symlink %s -> %s", a.Symlink.Newpath, a.Symlink.Oldpath))
		case *pb.FileAction_Rm:
			names = append(names, fmt.Sprintf("rm %s", a.Rm.Path))
		case *pb.FileAction_Copy:
			names = append(names, fmt.Sprintf("copy %s %s", a.Copy.Src, a.Copy.Dest))
		}
	}

	return strings.Join(names, ", ")
}
