// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package bots

import (
	"context"
	"encoding/json"
	"fmt"
	"time"

	"code.gitea.io/gitea/models/db"
	repo_model "code.gitea.io/gitea/models/repo"
	user_model "code.gitea.io/gitea/models/user"
	"code.gitea.io/gitea/models/webhook"
	api "code.gitea.io/gitea/modules/structs"
	"code.gitea.io/gitea/modules/timeutil"

	"github.com/nektos/act/pkg/jobparser"
	"golang.org/x/exp/slices"
	"xorm.io/builder"
)

// Run represents a run of a workflow file
type Run struct {
	ID            int64
	Title         string
	RepoID        int64                  `xorm:"index unique(repo_index)"`
	Repo          *repo_model.Repository `xorm:"-"`
	OwnerID       int64                  `xorm:"index"`
	WorkflowID    string                 `xorm:"index"`                    // the name of workflow file
	Index         int64                  `xorm:"index unique(repo_index)"` // a unique number for each run of a repository
	TriggerUserID int64
	TriggerUser   *user_model.User `xorm:"-"`
	Ref           string
	CommitSHA     string
	Event         webhook.HookEventType
	Token         string // token for this task
	Grant         string // permissions for this task
	EventPayload  string `xorm:"LONGTEXT"`
	Status        Status `xorm:"index"`
	Started       timeutil.TimeStamp
	Stopped       timeutil.TimeStamp
	Created       timeutil.TimeStamp `xorm:"created"`
	Updated       timeutil.TimeStamp `xorm:"updated"`
}

func init() {
	db.RegisterModel(new(Run))
	db.RegisterModel(new(RunIndex))
}

func (Run) TableName() string {
	return "bots_run"
}

func (run *Run) HTMLURL() string {
	return fmt.Sprintf("%s/builds/runs/%d", run.Repo.HTMLURL(), run.Index)
}

// LoadAttributes load Repo TriggerUser if not loaded
func (run *Run) LoadAttributes(ctx context.Context) error {
	if run == nil {
		return nil
	}

	if run.Repo == nil {
		repo, err := repo_model.GetRepositoryByIDCtx(ctx, run.RepoID)
		if err != nil {
			return err
		}
		run.Repo = repo
	}
	if err := run.Repo.LoadAttributes(ctx); err != nil {
		return err
	}

	if run.TriggerUser == nil {
		u, err := user_model.GetPossbileUserByID(ctx, run.TriggerUserID)
		if err != nil {
			return err
		}
		run.TriggerUser = u
	}

	return nil
}

func (run *Run) TakeTime() time.Duration {
	if run.Started == 0 {
		return 0
	}
	started := run.Started.AsTime()
	if run.Status.IsDone() {
		return run.Stopped.AsTime().Sub(started)
	}
	run.Stopped.AsTime().Sub(started)
	return time.Since(started).Truncate(time.Second)
}

func (run *Run) GetPushEventPayload() (*api.PushPayload, error) {
	if run.Event == webhook.HookEventPush {
		var payload api.PushPayload
		if err := json.Unmarshal([]byte(run.EventPayload), &payload); err != nil {
			return nil, err
		}
		return &payload, nil
	}
	return nil, fmt.Errorf("event %s is not a push event", run.Event)
}

func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) error {
	_, err := db.GetEngine(ctx).ID(repo.ID).
		SetExpr("num_runs",
			builder.Select("count(*)").From("bots_run").
				Where(builder.Eq{"repo_id": repo.ID}),
		).
		SetExpr("num_closed_runs",
			builder.Select("count(*)").From("bots_run").
				Where(builder.Eq{
					"repo_id": repo.ID,
				}.And(
					builder.In("status",
						StatusSuccess,
						StatusFailure,
						StatusCancelled,
						StatusSkipped,
					),
				),
				),
		).
		Update(repo)
	return err
}

// InsertRun inserts a bot run
func InsertRun(run *Run, jobs []*jobparser.SingleWorkflow) error {
	ctx, commiter, err := db.TxContext(db.DefaultContext)
	if err != nil {
		return err
	}
	defer commiter.Close()

	index, err := db.GetNextResourceIndex(ctx, "bots_run_index", run.RepoID)
	if err != nil {
		return err
	}
	run.Index = index

	if run.Status.IsUnknown() {
		run.Status = StatusWaiting
	}

	if err := db.Insert(ctx, run); err != nil {
		return err
	}

	if run.Repo == nil {
		repo, err := repo_model.GetRepositoryByIDCtx(ctx, run.RepoID)
		if err != nil {
			return err
		}
		run.Repo = repo
	}

	if err := updateRepoRunsNumbers(ctx, run.Repo); err != nil {
		return err
	}

	runJobs := make([]*RunJob, 0, len(jobs))
	for _, v := range jobs {
		id, job := v.Job()
		needs := job.Needs()
		job.EraseNeeds()
		payload, _ := v.Marshal()
		status := StatusWaiting
		if len(needs) > 0 {
			status = StatusBlocked
		}
		runJobs = append(runJobs, &RunJob{
			RunID:           run.ID,
			RepoID:          run.RepoID,
			OwnerID:         run.OwnerID,
			CommitSHA:       run.CommitSHA,
			Name:            job.Name,
			WorkflowPayload: payload,
			JobID:           id,
			Needs:           needs,
			RunsOn:          job.RunsOn(),
			Status:          status,
		})
	}
	if err := db.Insert(ctx, runJobs); err != nil {
		return err
	}

	return commiter.Commit()
}

// ErrRunNotExist represents an error for bot run not exist
type ErrRunNotExist struct {
	ID     int64
	RepoID int64
	Index  int64
}

func (err ErrRunNotExist) Error() string {
	if err.RepoID > 0 {
		return fmt.Sprintf("run repe_id [%d] index [%d] is not exist", err.RepoID, err.Index)
	}
	return fmt.Sprintf("run [%d] is not exist", err.ID)
}

func GetRunByID(ctx context.Context, id int64) (*Run, error) {
	var run Run
	has, err := db.GetEngine(ctx).Where("id=?", id).Get(&run)
	if err != nil {
		return nil, err
	} else if !has {
		return nil, ErrRunNotExist{
			ID: id,
		}
	}

	return &run, nil
}

func GetRunByIndex(ctx context.Context, repoID, index int64) (*Run, error) {
	run := &Run{
		RepoID: repoID,
		Index:  index,
	}
	has, err := db.GetEngine(ctx).Get(run)
	if err != nil {
		return nil, err
	} else if !has {
		return nil, ErrRunNotExist{
			RepoID: repoID,
			Index:  index,
		}
	}

	return run, nil
}

func UpdateRun(ctx context.Context, run *Run, cols ...string) error {
	sess := db.GetEngine(ctx).ID(run.ID)
	if len(cols) > 0 {
		sess.Cols(cols...)
	}
	_, err := sess.Update(run)

	if run.Status != 0 || slices.Contains(cols, "status") {
		if run.RepoID == 0 {
			run, err = GetRunByID(ctx, run.ID)
			if err != nil {
				return err
			}
		}
		if run.Repo == nil {
			repo, err := repo_model.GetRepositoryByIDCtx(ctx, run.RepoID)
			if err != nil {
				return err
			}
			run.Repo = repo
		}
		if err := updateRepoRunsNumbers(ctx, run.Repo); err != nil {
			return err
		}
	}

	return err
}

type RunIndex db.ResourceIndex

func (RunIndex) TableName() string {
	return "bots_run_index"
}
