package task

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

	bkplatforms "github.com/containerd/containerd/platforms"
	"github.com/moby/buildkit/client/llb"
	"github.com/moby/buildkit/exporter/containerimage/exptypes"
	dockerfilebuilder "github.com/moby/buildkit/frontend/dockerfile/builder"
	"github.com/moby/buildkit/frontend/dockerfile/dockerfile2llb"
	bkgw "github.com/moby/buildkit/frontend/gateway/client"
	bkpb "github.com/moby/buildkit/solver/pb"
	"github.com/rs/zerolog/log"

	"go.dagger.io/dagger/compiler"
	"go.dagger.io/dagger/plancontext"
	"go.dagger.io/dagger/solver"
)

func init() {
	Register("Build", func() Task { return &buildTask{} })
}

type buildTask struct {
}

func (t *buildTask) Run(ctx context.Context, pctx *plancontext.Context, s solver.Solver, v *compiler.Value) (*compiler.Value, error) {
	frontend, err := v.Lookup("frontend").String()
	if err != nil {
		return nil, err
	}

	switch frontend {
	case "dockerfile":
		return t.dockerfile(ctx, pctx, s, v)
	default:
		return nil, fmt.Errorf("unsupported frontend %q", frontend)
	}
}

func (t *buildTask) dockerfile(ctx context.Context, pctx *plancontext.Context, s solver.Solver, v *compiler.Value) (*compiler.Value, error) {
	lg := log.Ctx(ctx)

	// Read auth info
	auth, err := decodeAuthValue(pctx, v.Lookup("auth"))
	if err != nil {
		return nil, err
	}
	for _, a := range auth {
		s.AddCredentials(a.Target, a.Username, a.Secret.PlainText())
		lg.Debug().Str("target", a.Target).Msg("add target credentials")
	}

	source, err := pctx.FS.FromValue(v.Lookup("source"))
	if err != nil {
		return nil, err
	}

	sourceSt, err := source.Result().ToState()
	if err != nil {
		return nil, err
	}

	// docker build context
	contextDef, err := s.Marshal(ctx, sourceSt)
	if err != nil {
		return nil, err
	}
	// Dockerfile context, default to docker build context
	dockerfileDef := contextDef

	// Support inlined dockerfile
	if dockerfile := v.Lookup("dockerfile.contents"); dockerfile.Exists() {
		contents, err := dockerfile.String()
		if err != nil {
			return nil, err
		}
		dockerfileDef, err = s.Marshal(ctx,
			llb.Scratch().File(
				llb.Mkfile("/Dockerfile", 0644, []byte(contents)),
			),
		)
		if err != nil {
			return nil, err
		}
	}

	opts, err := t.dockerBuildOpts(v, pctx)
	if err != nil {
		return nil, err
	}
	// Handle --no-cache
	if s.NoCache() {
		opts["no-cache"] = ""
	}

	req := bkgw.SolveRequest{
		Frontend:    "dockerfile.v0",
		FrontendOpt: opts,
		FrontendInputs: map[string]*bkpb.Definition{
			dockerfilebuilder.DefaultLocalNameContext:    contextDef,
			dockerfilebuilder.DefaultLocalNameDockerfile: dockerfileDef,
		},
	}
	res, err := s.SolveRequest(ctx, req)
	if err != nil {
		return nil, err
	}
	ref, err := res.SingleRef()
	if err != nil {
		return nil, err
	}

	// Image metadata
	meta, ok := res.Metadata[exptypes.ExporterImageConfigKey]
	if !ok {
		return nil, errors.New("build returned no image config")
	}
	var image dockerfile2llb.Image
	if err := json.Unmarshal(meta, &image); err != nil {
		return nil, fmt.Errorf("failed to unmarshal image config: %w", err)
	}

	return compiler.NewValue().FillFields(map[string]interface{}{
		"output": pctx.FS.New(ref).MarshalCUE(),
		"config": image.Config,
	})
}

func (t *buildTask) dockerBuildOpts(v *compiler.Value, pctx *plancontext.Context) (map[string]string, error) {
	opts := map[string]string{}

	if dockerfilePath := v.Lookup("dockerfile.path"); dockerfilePath.Exists() {
		filename, err := dockerfilePath.String()
		if err != nil {
			return nil, err
		}
		opts["filename"] = filename
	}

	if target := v.Lookup("target"); target.Exists() {
		tgr, err := target.String()
		if err != nil {
			return nil, err
		}
		opts["target"] = tgr
	}

	if hosts := v.Lookup("hosts"); hosts.Exists() {
		p := []string{}
		fields, err := hosts.Fields()
		if err != nil {
			return nil, err
		}
		for _, host := range fields {
			s, err := host.Value.String()
			if err != nil {
				return nil, err
			}
			p = append(p, host.Label()+"="+s)
		}
		if len(p) > 0 {
			opts["add-hosts"] = strings.Join(p, ",")
		}
	}

	if buildArgs := v.Lookup("buildArg"); buildArgs.Exists() {
		fields, err := buildArgs.Fields()
		if err != nil {
			return nil, err
		}
		for _, buildArg := range fields {
			s, err := buildArg.Value.String()
			if err != nil {
				return nil, err
			}
			opts["build-arg:"+buildArg.Label()] = s
		}
	}

	if labels := v.Lookup("label"); labels.Exists() {
		fields, err := labels.Fields()
		if err != nil {
			return nil, err
		}
		for _, label := range fields {
			s, err := label.Value.String()
			if err != nil {
				return nil, err
			}
			opts["label:"+label.Label()] = s
		}
	}

	if platforms := v.Lookup("platforms"); platforms.Exists() {
		p := []string{}
		list, err := platforms.List()
		if err != nil {
			return nil, err
		}

		for _, platform := range list {
			s, err := platform.String()
			if err != nil {
				return nil, err
			}
			p = append(p, s)
		}

		if len(p) > 0 {
			opts["platform"] = strings.Join(p, ",")
		}
		if len(p) > 1 {
			opts["multi-platform"] = "true"
		}
	}
	// Set platform to configured one if no one is defined
	if opts["platform"] == "" {
		opts["platform"] = bkplatforms.Format(pctx.Platform.Get())
	}

	return opts, nil
}