package model

import (
	"context"
	"fmt"
	"strings"

	"github.com/nektos/act/pkg/common"
	"github.com/nektos/act/pkg/common/git"
)

type GithubContext struct {
	Event            map[string]interface{} `json:"event"`
	EventPath        string                 `json:"event_path"`
	Workflow         string                 `json:"workflow"`
	RunAttempt       string                 `json:"run_attempt"`
	RunID            string                 `json:"run_id"`
	RunNumber        string                 `json:"run_number"`
	Actor            string                 `json:"actor"`
	Repository       string                 `json:"repository"`
	EventName        string                 `json:"event_name"`
	Sha              string                 `json:"sha"`
	Ref              string                 `json:"ref"`
	RefName          string                 `json:"ref_name"`
	RefType          string                 `json:"ref_type"`
	HeadRef          string                 `json:"head_ref"`
	BaseRef          string                 `json:"base_ref"`
	Token            string                 `json:"token"`
	Workspace        string                 `json:"workspace"`
	Action           string                 `json:"action"`
	ActionPath       string                 `json:"action_path"`
	ActionRef        string                 `json:"action_ref"`
	ActionRepository string                 `json:"action_repository"`
	Job              string                 `json:"job"`
	JobName          string                 `json:"job_name"`
	RepositoryOwner  string                 `json:"repository_owner"`
	RetentionDays    string                 `json:"retention_days"`
	RunnerPerflog    string                 `json:"runner_perflog"`
	RunnerTrackingID string                 `json:"runner_tracking_id"`
	ServerURL        string                 `json:"server_url"`
	APIURL           string                 `json:"api_url"`
	GraphQLURL       string                 `json:"graphql_url"`
}

func asString(v interface{}) string {
	if v == nil {
		return ""
	} else if s, ok := v.(string); ok {
		return s
	}
	return ""
}

func nestedMapLookup(m map[string]interface{}, ks ...string) (rval interface{}) {
	var ok bool

	if len(ks) == 0 { // degenerate input
		return nil
	}
	if rval, ok = m[ks[0]]; !ok {
		return nil
	} else if len(ks) == 1 { // we've reached the final key
		return rval
	} else if m, ok = rval.(map[string]interface{}); !ok {
		return nil
	} else { // 1+ more keys
		return nestedMapLookup(m, ks[1:]...)
	}
}

func withDefaultBranch(ctx context.Context, b string, event map[string]interface{}) map[string]interface{} {
	repoI, ok := event["repository"]
	if !ok {
		repoI = make(map[string]interface{})
	}

	repo, ok := repoI.(map[string]interface{})
	if !ok {
		common.Logger(ctx).Warnf("unable to set default branch to %v", b)
		return event
	}

	// if the branch is already there return with no changes
	if _, ok = repo["default_branch"]; ok {
		return event
	}

	repo["default_branch"] = b
	event["repository"] = repo

	return event
}

var findGitRef = git.FindGitRef
var findGitRevision = git.FindGitRevision

func (ghc *GithubContext) SetRef(ctx context.Context, defaultBranch string, repoPath string) {
	logger := common.Logger(ctx)

	// https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows
	// https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads
	switch ghc.EventName {
	case "pull_request_target":
		ghc.Ref = fmt.Sprintf("refs/heads/%s", ghc.BaseRef)
	case "pull_request", "pull_request_review", "pull_request_review_comment":
		ghc.Ref = fmt.Sprintf("refs/pull/%.0f/merge", ghc.Event["number"])
	case "deployment", "deployment_status":
		ghc.Ref = asString(nestedMapLookup(ghc.Event, "deployment", "ref"))
	case "release":
		ghc.Ref = fmt.Sprintf("refs/tags/%s", asString(nestedMapLookup(ghc.Event, "release", "tag_name")))
	case "push", "create", "workflow_dispatch":
		ghc.Ref = asString(ghc.Event["ref"])
	default:
		defaultBranch := asString(nestedMapLookup(ghc.Event, "repository", "default_branch"))
		if defaultBranch != "" {
			ghc.Ref = fmt.Sprintf("refs/heads/%s", defaultBranch)
		}
	}

	if ghc.Ref == "" {
		ref, err := findGitRef(ctx, repoPath)
		if err != nil {
			logger.Warningf("unable to get git ref: %v", err)
		} else {
			logger.Debugf("using github ref: %s", ref)
			ghc.Ref = ref
		}

		// set the branch in the event data
		if defaultBranch != "" {
			ghc.Event = withDefaultBranch(ctx, defaultBranch, ghc.Event)
		} else {
			ghc.Event = withDefaultBranch(ctx, "master", ghc.Event)
		}

		if ghc.Ref == "" {
			ghc.Ref = fmt.Sprintf("refs/heads/%s", asString(nestedMapLookup(ghc.Event, "repository", "default_branch")))
		}
	}
}

func (ghc *GithubContext) SetSha(ctx context.Context, repoPath string) {
	logger := common.Logger(ctx)

	// https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows
	// https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads
	switch ghc.EventName {
	case "pull_request_target":
		ghc.Sha = asString(nestedMapLookup(ghc.Event, "pull_request", "base", "sha"))
	case "deployment", "deployment_status":
		ghc.Sha = asString(nestedMapLookup(ghc.Event, "deployment", "sha"))
	case "push", "create", "workflow_dispatch":
		if deleted, ok := ghc.Event["deleted"].(bool); ok && !deleted {
			ghc.Sha = asString(ghc.Event["after"])
		}
	}

	if ghc.Sha == "" {
		_, sha, err := findGitRevision(ctx, repoPath)
		if err != nil {
			logger.Warningf("unable to get git revision: %v", err)
		} else {
			ghc.Sha = sha
		}
	}
}

func (ghc *GithubContext) SetRepositoryAndOwner(ctx context.Context, githubInstance string, remoteName string, repoPath string) {
	if ghc.Repository == "" {
		repo, err := git.FindGithubRepo(ctx, repoPath, githubInstance, remoteName)
		if err != nil {
			common.Logger(ctx).Warningf("unable to get git repo (githubInstance: %v; remoteName: %v, repoPath: %v): %v", githubInstance, remoteName, repoPath, err)
			return
		}
		ghc.Repository = repo
	}
	ghc.RepositoryOwner = strings.Split(ghc.Repository, "/")[0]
}

func (ghc *GithubContext) SetRefTypeAndName() {
	var refType, refName string

	// https://docs.github.com/en/actions/learn-github-actions/environment-variables
	if strings.HasPrefix(ghc.Ref, "refs/tags/") {
		refType = "tag"
		refName = ghc.Ref[len("refs/tags/"):]
	} else if strings.HasPrefix(ghc.Ref, "refs/heads/") {
		refType = "branch"
		refName = ghc.Ref[len("refs/heads/"):]
	} else if strings.HasPrefix(ghc.Ref, "refs/pull/") {
		refType = ""
		refName = ghc.Ref[len("refs/pull/"):]
	}

	if ghc.RefType == "" {
		ghc.RefType = refType
	}

	if ghc.RefName == "" {
		ghc.RefName = refName
	}
}

func (ghc *GithubContext) SetBaseAndHeadRef() {
	if ghc.EventName == "pull_request" || ghc.EventName == "pull_request_target" {
		if ghc.BaseRef == "" {
			ghc.BaseRef = asString(nestedMapLookup(ghc.Event, "pull_request", "base", "ref"))
		}

		if ghc.HeadRef == "" {
			ghc.HeadRef = asString(nestedMapLookup(ghc.Event, "pull_request", "head", "ref"))
		}
	}
}
