package pulls

import (
	"encoding/json"
	"fmt"
	"net/http"
	"slices"
	"time"

	indigoxrpc "github.com/bluesky-social/indigo/xrpc"

	"tangled.org/core/api/tangled"
	"tangled.org/core/appview/db"
	"tangled.org/core/appview/models"
	"tangled.org/core/hostutil"
	"tangled.org/core/types"
)

// spindle upgrades may change how a commit is resolved and hashed (even if very rare...)
const (
	workflowCacheSize = 4096
	workflowCacheTTL  = time.Hour
)

func (s *Pulls) describeWorkflowAt(r *http.Request, f *models.Repo, sha, sourceRepo string) (*tangled.CiDescribeWorkflowDefinition_Output, error) {
	key := fmt.Sprintf("%s|%s|%s|%s", f.Spindle, f.RepoDid, sha, sourceRepo)
	if cached, ok := s.workflowDescCache.Get(key); ok {
		return cached, nil
	}

	spindleUrl, err := hostutil.EnsureHttpScheme(f.Spindle)
	if err != nil {
		return nil, err
	}
	client := &indigoxrpc.Client{Host: spindleUrl}
	out, err := tangled.CiDescribeWorkflowDefinition(r.Context(), client, f.RepoDid, sha, sourceRepo)
	if err != nil {
		return nil, err
	}

	s.workflowDescCache.Add(key, out)
	return out, nil
}

// returns the merge-base of the pull's source and target branches.
//
// older rounds resolve it live on the fork's knot, which tracks the
// target branch as refs/hidden/<source>/<target>.
func (s *Pulls) mergeBase(r *http.Request, pull *models.Pull) (string, error) {
	if mb := pull.LatestSubmission().MergeBase; mb != "" {
		return mb, nil
	}

	sourceBranch := pull.PullSource.Branch
	hiddenRef := fmt.Sprintf("hidden/%s/%s", sourceBranch, pull.TargetBranch)

	key := fmt.Sprintf("%s|%s|%s", pull.PullSource.RepoDid, sourceBranch, pull.TargetBranch)
	if cached, ok := s.mergeBaseCache.Get(key); ok {
		return cached, nil
	}

	forkRepo, err := db.GetRepoByDid(s.db, pull.PullSource.RepoDid.String())
	if err != nil {
		return "", fmt.Errorf("resolving fork repo: %w", err)
	}
	compareBytes, err := tangled.RepoCompare(r.Context(), s.knotClient(forkRepo.Knot), forkRepo.RepoIdentifier(), hiddenRef, sourceBranch)
	if err != nil {
		return "", fmt.Errorf("comparing %s...%s: %w", hiddenRef, sourceBranch, err)
	}
	var comparison types.RepoFormatPatchResponse
	if err := json.Unmarshal(compareBytes, &comparison); err != nil {
		return "", fmt.Errorf("decoding comparison: %w", err)
	}
	if comparison.MergeBase == "" {
		return "", fmt.Errorf("no merge base between %s and %s", hiddenRef, sourceBranch)
	}

	s.mergeBaseCache.Add(key, comparison.MergeBase)
	return comparison.MergeBase, nil
}

// compares the spindle's workflow-definition fingerprints at the
// pull head and its merge-base with the target branch.
func (s *Pulls) workflowChangeFromSpindle(r *http.Request, f *models.Repo, pull *models.Pull) (bool, []string, error) {
	sourceRepo := pull.PullSource.RepoDid.String()

	head, err := s.describeWorkflowAt(r, f, pull.LatestSha(), sourceRepo)
	if err != nil {
		return false, nil, err
	}
	// a definition not derived from the repo cannot change between commits
	if !head.Derived {
		return false, nil, nil
	}

	baseSha, err := s.mergeBase(r, pull)
	if err != nil {
		return false, nil, err
	}
	base, err := s.describeWorkflowAt(r, f, baseSha, sourceRepo)
	if err != nil {
		return false, nil, err
	}
	if !base.Derived {
		return false, nil, nil
	}
	if head.Hash != nil && base.Hash != nil && *head.Hash == *base.Hash {
		return false, nil, nil
	}

	names := slices.Concat(head.Workflows, base.Workflows)
	slices.Sort(names)
	return true, slices.Compact(names), nil
}
