package client

import (
	"context"
	"io"
	"maps"
	"slices"
	"syscall"

	"github.com/moby/buildkit/client/llb"
	"github.com/moby/buildkit/client/llb/sourceresolver"
	"github.com/moby/buildkit/solver/pb"
	"github.com/moby/buildkit/solver/result"
	spb "github.com/moby/buildkit/sourcepolicy/pb"
	"github.com/moby/buildkit/util/apicaps"
	digest "github.com/opencontainers/go-digest"
	ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
	fstypes "github.com/tonistiigi/fsutil/types"
)

type Result = result.Result[Reference]

type Attestation = result.Attestation[Reference]

type BuildFunc func(context.Context, Client) (*Result, error)

func NewResult() *Result {
	return &Result{}
}

type Client interface {
	sourceresolver.MetaResolver
	Solve(ctx context.Context, req SolveRequest) (*Result, error)
	ResolveImageConfig(ctx context.Context, ref string, opt sourceresolver.Opt) (string, digest.Digest, []byte, error)
	BuildOpts() BuildOpts
	Inputs(ctx context.Context) (map[string]llb.State, error)
	NewContainer(ctx context.Context, req NewContainerRequest) (Container, error)
	Warn(ctx context.Context, dgst digest.Digest, msg string, opts WarnOpts) error
}

// NewContainerRequest encapsulates the requirements for a client to define a
// new container, without defining the initial process.
type NewContainerRequest struct {
	Mounts      []Mount
	Hostname    string
	NetMode     pb.NetMode
	ExtraHosts  []*pb.HostIP
	Platform    *pb.Platform
	Constraints *pb.WorkerConstraints
}

// Mount allows clients to specify a filesystem mount. A Reference to a
// previously solved Result is required.
type Mount struct {
	Selector  string
	Dest      string
	ResultID  string
	Ref       Reference
	Readonly  bool
	MountType pb.MountType
	CacheOpt  *pb.CacheOpt
	SecretOpt *pb.SecretOpt
	SSHOpt    *pb.SSHOpt
}

// Container is used to start new processes inside a container and release the
// container resources when done.
type Container interface {
	Start(context.Context, StartRequest) (ContainerProcess, error)
	Release(context.Context) error
	ReadFile(ctx context.Context, req ReadContainerRequest) ([]byte, error)
	StatFile(ctx context.Context, req StatContainerRequest) (*fstypes.Stat, error)
	ReadDir(ctx context.Context, req ReadDirContainerRequest) ([]*fstypes.Stat, error)
}

// StartRequest encapsulates the arguments to define a process within a
// container.
type StartRequest struct {
	Args           []string
	Env            []string
	SecretEnv      []*pb.SecretEnv
	User           string
	Cwd            string
	Tty            bool
	Stdin          io.ReadCloser
	Stdout, Stderr io.WriteCloser
	SecurityMode   pb.SecurityMode

	RemoveMountStubsRecursive bool
}

// WinSize is same as executor.WinSize, copied here to prevent circular package
// dependencies.
type WinSize struct {
	Rows uint32
	Cols uint32
}

// ContainerProcess represents a process within a container.
type ContainerProcess interface {
	Wait() error
	Resize(ctx context.Context, size WinSize) error
	Signal(ctx context.Context, sig syscall.Signal) error
}

type Reference interface {
	ToState() (llb.State, error)
	Evaluate(ctx context.Context) error
	ReadFile(ctx context.Context, req ReadRequest) ([]byte, error)
	StatFile(ctx context.Context, req StatRequest) (*fstypes.Stat, error)
	ReadDir(ctx context.Context, req ReadDirRequest) ([]*fstypes.Stat, error)
}

type ReadRequest struct {
	Filename string
	Range    *FileRange
}

type ReadContainerRequest struct {
	ReadRequest
	MountIndex int
}

type FileRange struct {
	Offset int
	Length int
}

type ReadDirRequest struct {
	Path           string
	IncludePattern string
}

type ReadDirContainerRequest struct {
	ReadDirRequest
	MountIndex int
}

type StatRequest struct {
	Path string
}

type StatContainerRequest struct {
	StatRequest
	MountIndex int
}

// SolveRequest is same as frontend.SolveRequest but avoiding dependency
type SolveRequest struct {
	Evaluate       bool
	Definition     *pb.Definition
	Frontend       string
	FrontendOpt    map[string]string
	FrontendInputs map[string]*pb.Definition
	CacheImports   []CacheOptionsEntry
	SourcePolicies []*spb.Policy
}

// Clone returns a deep copy of the solve request.
func (r SolveRequest) Clone() SolveRequest {
	if r.Definition != nil {
		r.Definition = r.Definition.CloneVT()
	}
	r.FrontendOpt = maps.Clone(r.FrontendOpt)
	if len(r.FrontendInputs) > 0 {
		inputs := r.FrontendInputs
		r.FrontendInputs = make(map[string]*pb.Definition, len(inputs))
		for k, v := range inputs {
			if v != nil {
				v = v.CloneVT()
			}
			r.FrontendInputs[k] = v
		}
	}
	if len(r.CacheImports) > 0 {
		r.CacheImports = slices.Clone(r.CacheImports)
		for i, ci := range r.CacheImports {
			ci.Attrs = maps.Clone(ci.Attrs)
			r.CacheImports[i] = ci
		}
	}
	if len(r.SourcePolicies) > 0 {
		r.SourcePolicies = slices.Clone(r.SourcePolicies)
		for i, p := range r.SourcePolicies {
			if p != nil {
				r.SourcePolicies[i] = p.CloneVT()
			}
		}
	}
	return r
}

type CacheOptionsEntry struct {
	Type  string
	Attrs map[string]string
}

type WorkerInfo struct {
	ID        string
	Labels    map[string]string
	Platforms []ocispecs.Platform
}

type BuildOpts struct {
	Opts      map[string]string
	SessionID string
	Workers   []WorkerInfo
	Product   string
	LLBCaps   apicaps.CapSet
	Caps      apicaps.CapSet
}

type WarnOpts struct {
	Level      int
	SourceInfo *pb.SourceInfo
	Range      []*pb.Range
	Detail     [][]byte
	URL        string
}
