package task

import (
	"context"
	"errors"
	"fmt"
	"os"

	"cuelang.org/go/cue"
	"github.com/moby/buildkit/client/llb"
	"github.com/rs/zerolog/log"
	"go.dagger.io/dagger/compiler"
	"go.dagger.io/dagger/plancontext"
	"go.dagger.io/dagger/solver"
)

func init() {
	Register("ClientFilesystemRead", func() Task { return &clientFilesystemReadTask{} })
}

type clientFilesystemReadTask struct {
}

func (t clientFilesystemReadTask) PreRun(ctx context.Context, pctx *plancontext.Context, v *compiler.Value) error {
	path, err := t.parsePath(v)
	if err != nil {
		return err
	}

	if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
		return fmt.Errorf("path %q does not exist", path)
	}

	if plancontext.IsFSValue(v.Lookup("contents")) {
		pctx.LocalDirs.Add(path)
	}

	return nil
}

func (t clientFilesystemReadTask) Run(ctx context.Context, pctx *plancontext.Context, s solver.Solver, v *compiler.Value) (*compiler.Value, error) {
	path, err := t.parsePath(v)
	if err != nil {
		return nil, err
	}

	contents, err := t.readContents(ctx, pctx, s, v, path)
	if err != nil {
		return nil, err
	}

	return compiler.NewValue().FillFields(map[string]interface{}{
		"contents": contents,
	})
}

func (t clientFilesystemReadTask) parsePath(v *compiler.Value) (path string, err error) {
	path, err = v.Lookup("path").String()
	if err != nil {
		return
	}

	// Keep socket paths as is (e.g., npipe)
	if plancontext.IsServiceValue(v.Lookup("contents")) {
		return
	}

	path, err = clientFilePath(path)

	return
}

func (t clientFilesystemReadTask) readContents(ctx context.Context, pctx *plancontext.Context, s solver.Solver, v *compiler.Value, path string) (interface{}, error) {
	lg := log.Ctx(ctx)
	contents := v.Lookup("contents")

	if plancontext.IsFSValue(contents) {
		lg.Debug().Str("path", path).Msg("loading local directory")
		return t.readFS(ctx, pctx, s, v, path)
	}

	if plancontext.IsSecretValue(contents) {
		lg.Debug().Str("path", path).Msg("loading local secret file")
		return t.readSecret(pctx, path)
	}

	if contents.IsConcrete() {
		return nil, fmt.Errorf("unexpected concrete value, please use a type")
	}

	k := contents.IncompleteKind()
	if k == cue.StringKind {
		lg.Debug().Str("path", path).Msg("loading local file")
		return t.readString(path)
	}

	return nil, fmt.Errorf("unsupported type %q", k)
}

func (t clientFilesystemReadTask) readFS(ctx context.Context, pctx *plancontext.Context, s solver.Solver, v *compiler.Value, path string) (*compiler.Value, error) {
	var dir struct {
		Include []string
		Exclude []string
	}

	if err := v.Decode(&dir); err != nil {
		return nil, err
	}

	opts := []llb.LocalOption{
		withCustomName(v, "Local %s", path),
		// Without hint, multiple `llb.Local` operations on the
		// same path get a different digest.
		llb.SessionID(s.SessionID()),
		llb.SharedKeyHint(path),
	}

	if len(dir.Include) > 0 {
		opts = append(opts, llb.IncludePatterns(dir.Include))
	}

	// Excludes .dagger directory by default
	excludePatterns := []string{"**/.dagger/"}
	if len(dir.Exclude) > 0 {
		excludePatterns = dir.Exclude
	}
	opts = append(opts, llb.ExcludePatterns(excludePatterns))

	// FIXME: Remove the `Copy` and use `Local` directly.
	//
	// Copy'ing is a costly operation which should be unnecessary.
	// However, using llb.Local directly breaks caching sometimes for unknown reasons.
	st := llb.Scratch().File(
		llb.Copy(
			llb.Local(
				path,
				opts...,
			),
			"/",
			"/",
		),
		withCustomName(v, "Local %s [copy]", path),
	)

	result, err := s.Solve(ctx, st, pctx.Platform.Get())
	if err != nil {
		return nil, err
	}

	fs := pctx.FS.New(result)
	return fs.MarshalCUE(), nil
}

func (t clientFilesystemReadTask) readSecret(pctx *plancontext.Context, path string) (*compiler.Value, error) {
	contents, err := t.readString(path)
	if err != nil {
		return nil, err
	}
	secret := pctx.Secrets.New(contents)
	return secret.MarshalCUE(), nil
}

func (t clientFilesystemReadTask) readString(path string) (string, error) {
	contents, err := os.ReadFile(path)
	if err != nil {
		return "", err
	}
	return string(contents), nil
}
