// Copyright The Forgejo Authors.
// SPDX-License-Identifier: MIT

package actions

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"strconv"

	actions_model "forgejo.org/models/actions"
	"forgejo.org/models/perm"
	"forgejo.org/models/perm/access"
	repo_model "forgejo.org/models/repo"
	"forgejo.org/models/user"
	"forgejo.org/modules/actions"
	"forgejo.org/modules/git"
	"forgejo.org/modules/json"
	"forgejo.org/modules/setting"
	"forgejo.org/modules/structs"
	"forgejo.org/modules/util"
	"forgejo.org/modules/webhook"
	"forgejo.org/services/convert"

	"code.forgejo.org/forgejo/runner/v11/act/jobparser"
	act_model "code.forgejo.org/forgejo/runner/v11/act/model"
)

type InputRequiredErr struct {
	Name string
}

func (err InputRequiredErr) Error() string {
	return fmt.Sprintf("input required for '%s'", err.Name)
}

func IsInputRequiredErr(err error) bool {
	_, ok := err.(InputRequiredErr)
	return ok
}

type Workflow struct {
	WorkflowID string
	Ref        string
	Commit     *git.Commit
	GitEntry   *git.TreeEntry
}

type InputValueGetter func(key string) string

func (entry *Workflow) Dispatch(ctx context.Context, inputGetter InputValueGetter, repo *repo_model.Repository, doer *user.User) (r *actions_model.ActionRun, j []string, err error) {
	content, err := actions.GetContentFromEntry(entry.GitEntry)
	if err != nil {
		return nil, nil, err
	}

	wf, err := act_model.ReadWorkflow(bytes.NewReader(content), false)
	if err != nil {
		return nil, nil, err
	}

	fullWorkflowID := ".forgejo/workflows/" + entry.WorkflowID

	title := wf.Name
	if len(title) < 1 {
		title = fullWorkflowID
	}

	inputs := make(map[string]string)
	if workflowDispatch := wf.WorkflowDispatchConfig(); workflowDispatch != nil {
		for key, input := range workflowDispatch.Inputs {
			val := inputGetter(key)
			if len(val) == 0 {
				val = input.Default
				if len(val) == 0 {
					if input.Required {
						name := input.Description
						if len(name) == 0 {
							name = key
						}
						return nil, nil, InputRequiredErr{Name: name}
					}
					continue
				}
			} else if input.Type == "boolean" {
				// Since "boolean" inputs are rendered as a checkbox in html, the value inside the form is "on"
				val = strconv.FormatBool(val == "on")
			}
			inputs[key] = val
		}
	}

	if int64(len(inputs)) > setting.Actions.LimitDispatchInputs {
		return nil, nil, errors.New("to many inputs")
	}

	jobNames := util.KeysOfMap(wf.Jobs)

	payload := &structs.WorkflowDispatchPayload{
		Inputs:     inputs,
		Ref:        entry.Ref,
		Repository: convert.ToRepo(ctx, repo, access.Permission{AccessMode: perm.AccessModeNone}),
		Sender:     convert.ToUser(ctx, doer, nil),
		Workflow:   fullWorkflowID,
	}

	p, err := json.Marshal(payload)
	if err != nil {
		return nil, nil, err
	}

	notifications, err := wf.Notifications()
	if err != nil {
		return nil, nil, err
	}

	run := &actions_model.ActionRun{
		Title:         title,
		RepoID:        repo.ID,
		Repo:          repo,
		OwnerID:       repo.OwnerID,
		WorkflowID:    entry.WorkflowID,
		TriggerUserID: doer.ID,
		TriggerUser:   doer,
		Ref:           entry.Ref,
		CommitSHA:     entry.Commit.ID.String(),
		Event:         webhook.HookEventWorkflowDispatch,
		EventPayload:  string(p),
		TriggerEvent:  string(webhook.HookEventWorkflowDispatch),
		Status:        actions_model.StatusWaiting,
		NotifyEmail:   notifications,
	}

	vars, err := actions_model.GetVariablesOfRun(ctx, run)
	if err != nil {
		return nil, nil, err
	}

	jobs, err := jobparser.Parse(content, false, jobparser.WithVars(vars))
	if err != nil {
		return nil, nil, err
	}

	return run, jobNames, actions_model.InsertRun(ctx, run, jobs)
}

func GetWorkflowFromCommit(gitRepo *git.Repository, ref, workflowID string) (*Workflow, error) {
	ref, err := gitRepo.ExpandRef(ref)
	if err != nil {
		return nil, err
	}

	commit, err := gitRepo.GetCommit(ref)
	if err != nil {
		return nil, err
	}

	entries, err := actions.ListWorkflows(commit)
	if err != nil {
		return nil, err
	}

	var workflowEntry *git.TreeEntry
	for _, entry := range entries {
		if entry.Name() == workflowID {
			workflowEntry = entry
			break
		}
	}
	if workflowEntry == nil {
		return nil, errors.New("workflow not found")
	}

	return &Workflow{
		WorkflowID: workflowID,
		Ref:        ref,
		Commit:     commit,
		GitEntry:   workflowEntry,
	}, nil
}

// Sets the ConcurrencyGroup & ConcurrencyType on the provided ActionRun based upon the Workflow's `concurrency` data,
// or appropriate defaults if not present.
func ConfigureActionRunConcurrency(workflow *act_model.Workflow, run *actions_model.ActionRun, vars map[string]string, inputs map[string]any) error {
	_, cancelInProgress, err := jobparser.EvaluateWorkflowConcurrency(
		workflow.RawConcurrency, generateGiteaContextForRun(run), vars, inputs)
	if err != nil {
		return fmt.Errorf("unable to evaluate workflow `concurrency` block: %w", err)
	}
	if cancelInProgress == nil {
		// Maintain compatible behavior from before concurrency groups were implemented -- if `cancel-in-progress`
		// isn't defined in the workflow, cancel on push & PR sync events.
		if run.Event == webhook.HookEventPush || run.Event == webhook.HookEventPullRequestSync {
			run.ConcurrencyType = actions_model.CancelInProgress
		} else {
			run.ConcurrencyType = actions_model.UnlimitedConcurrency
		}
	} else if *cancelInProgress {
		run.ConcurrencyType = actions_model.CancelInProgress
	} else {
		// A workflow has explicitly listed `cancel-in-progress: false`, and we don't support concurrency groups
		// (queue-behind style behaviour, to be added in Forgejo v14).
		run.ConcurrencyType = actions_model.UnlimitedConcurrency
	}
	return nil
}
