package plan

import (
	"context"
	"errors"
	"fmt"
	"strings"

	"cuelang.org/go/cue"
	cueflow "cuelang.org/go/tools/flow"
	"github.com/rs/zerolog/log"
	"go.dagger.io/dagger/compiler"
	"go.dagger.io/dagger/pkg"
	"go.dagger.io/dagger/plan/task"
	"go.dagger.io/dagger/plancontext"
	"go.dagger.io/dagger/solver"
	"go.opentelemetry.io/otel"
)

var (
	ErrIncompatiblePlan = errors.New("attempting to load a dagger 0.1.0 project.\nPlease upgrade your config to be compatible with this version of dagger. Contact the Dagger team if you need help")
	ActionSelector      = cue.Str("actions")
	ClientSelector      = cue.Str("client")
)

type Plan struct {
	config Config

	context *plancontext.Context
	source  *compiler.Value
	action  *Action
}

type Config struct {
	Args   []string
	With   []string
	Target string
}

func Load(ctx context.Context, cfg Config) (*Plan, error) {
	log.Ctx(ctx).Debug().Interface("args", cfg.Args).Msg("loading plan")

	_, cueModExists := pkg.GetCueModParent()
	if !cueModExists {
		return nil, fmt.Errorf("dagger project not found. Run `dagger project init`")
	}

	v, err := compiler.Build("", nil, cfg.Args...)
	if err != nil {
		errstring := err.Error()

		if strings.Contains(errstring, "cannot find package") {
			if strings.Contains(errstring, "alpha.dagger.io") {
				return nil, ErrIncompatiblePlan
			} else if strings.Contains(errstring, pkg.DaggerModule) || strings.Contains(errstring, pkg.UniverseModule) {
				return nil, fmt.Errorf("%w: running `dagger project update` may resolve this", err)
			}
		}

		return nil, err
	}

	for i, param := range cfg.With {
		log.Ctx(ctx).Debug().Interface("with", param).Msg("compiling overlay")
		paramV, err := compiler.Compile(fmt.Sprintf("with%v", i), param)
		if err != nil {
			return nil, err
		}

		log.Ctx(ctx).Debug().Interface("with", param).Msg("filling overlay")
		fillErr := v.FillPath(cue.MakePath(), paramV)
		if fillErr != nil {
			return nil, fillErr
		}
	}

	p := &Plan{
		config:  cfg,
		context: plancontext.New(),
		source:  v,
	}

	p.fillAction()

	if err := p.configPlatform(); err != nil {
		return nil, err
	}

	if err := p.prepare(ctx); err != nil {
		return nil, err
	}

	return p, nil
}

func (p *Plan) Context() *plancontext.Context {
	return p.context
}

func (p *Plan) Source() *compiler.Value {
	return p.source
}

func (p *Plan) Action() *Action {
	return p.action
}

// configPlatform load the platform specified in the context
// Buildkit will then run every operation using that platform
// If platform is not define, context keep default platform
func (p *Plan) configPlatform() error {
	platformField := p.source.Lookup("platform")

	// Ignore if platform is not set in `#Plan`
	if !platformField.Exists() {
		return nil
	}

	// Convert platform to string
	platform, err := platformField.String()
	if err != nil {
		return err
	}

	// Set platform to context
	err = p.context.Platform.Set(platform)
	if err != nil {
		return err
	}
	return nil
}

// prepare executes the pre-run hooks of tasks
func (p *Plan) prepare(ctx context.Context) error {
	flow := cueflow.New(
		&cueflow.Config{
			FindHiddenTasks: true,
		},
		p.source.Cue(),
		func(flowVal cue.Value) (cueflow.Runner, error) {
			v := compiler.Wrap(flowVal)
			t, err := task.Lookup(v)
			if err != nil {
				// Not a task
				if err == task.ErrNotTask {
					return nil, nil
				}
				return nil, err
			}
			r, ok := t.(task.PreRunner)
			if !ok {
				return nil, nil
			}

			return cueflow.RunnerFunc(func(t *cueflow.Task) error {
				ctx := t.Context()
				lg := log.Ctx(ctx).With().Str("task", t.Path().String()).Logger()
				ctx = lg.WithContext(ctx)

				if err := r.PreRun(ctx, p.context, compiler.Wrap(t.Value())); err != nil {
					return fmt.Errorf("%s: %w", t.Path().String(), err)
				}
				return nil
			}), nil
		},
	)
	return flow.Run(ctx)
}

// Do executes an action in the plan
func (p *Plan) Do(ctx context.Context, path cue.Path, s solver.Solver) error {
	ctx, span := otel.Tracer("dagger").Start(ctx, "plan.Up")
	defer span.End()

	r := NewRunner(p.context, path, s)
	return r.Run(ctx, p.source)
}

func (p *Plan) fillAction() {
	cfg := &cueflow.Config{
		FindHiddenTasks: true,
		Root:            cue.MakePath(ActionSelector),
	}

	flow := cueflow.New(
		cfg,
		p.source.Cue(),
		noOpRunner,
	)

	p.action = &Action{
		Name:     ActionSelector.String(),
		Hidden:   false,
		Path:     cue.MakePath(ActionSelector),
		Children: []*Action{},
	}

	actions := p.source.LookupPath(cue.MakePath(ActionSelector))
	if !actions.Exists() {
		return
	}
	p.action.Documentation = actions.DocSummary()

	tasks := flow.Tasks()

	for _, t := range tasks {
		var q []cue.Selector
		prevAction := p.action
		for _, s := range t.Path().Selectors() {
			q = append(q, s)
			path := cue.MakePath(q...)
			a := prevAction.FindByPath(path)
			if a == nil {
				v := p.Source().LookupPath(path)
				a = &Action{
					Name:          s.String(),
					Hidden:        s.PkgPath() != "",
					Path:          path,
					Documentation: v.DocSummary(),
					Children:      []*Action{},
				}
				prevAction.AddChild(a)
			}
			prevAction = a
		}
	}
}
