//go:generate ${GOBIN}/mockgen -destination client_mock.go -package=byocimagesynccli ${GO_MODULE}/pkg/infra/clients/byocimagesynccli Client
package byocimagesynccli

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

	"github.com/google/go-github/v54/github"
	"github.com/tidbcloud/devops/pkg/infra/config"
	"golang.org/x/oauth2"
)

const (
	workflowRunLookupLimit = 100
	defaultWorkflowRef     = "main"
)

var workflowRunLookupInterval = 2 * time.Second

type Image struct {
	Source string `json:"source"`
	Digest string `json:"digest"`
}

type DispatchRequest struct {
	Environment   string
	Images        []Image
	CorrelationID string
	RequestedAt   time.Time
}

type WorkflowRun struct {
	ID         int64
	URL        string
	Status     string
	Conclusion string
	CreatedAt  time.Time
}

type Client interface {
	Dispatch(ctx context.Context, request DispatchRequest) (*WorkflowRun, error)
	GetRun(ctx context.Context, runID int64) (*WorkflowRun, error)
}

type client struct {
	github        *github.Client
	owner         string
	repo          string
	ref           string
	environment   string
	workflow      string
	dispatchMutex sync.Mutex
}

func NewClient(conf *config.RootConfig) (Client, error) {
	byocConf := conf.Services.KernelImage.BYOCSync
	if !conf.Services.KernelImage.Enabled || !byocConf.Enabled {
		return &client{}, nil
	}
	fidConf := conf.Services.KernelImage.FID
	if strings.TrimSpace(fidConf.RepositoryOwner) == "" || strings.TrimSpace(fidConf.RepositoryName) == "" {
		return nil, fmt.Errorf("kernel image FID repository owner and name are required for BYOC image sync")
	}
	if strings.TrimSpace(conf.Clients.Github.Token) == "" {
		return nil, fmt.Errorf("GitHub token is required for BYOC image sync")
	}

	source := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: conf.Clients.Github.Token})
	httpClient := oauth2.NewClient(context.Background(), source)
	httpClient.Timeout = 10 * time.Second
	ref := valueOrDefault(byocConf.Ref, defaultWorkflowRef)
	environment := strings.ToLower(strings.TrimSpace(conf.Services.KernelImage.LocalDevOpsEnvironment))
	if environment != "dev" && environment != "prod" {
		return nil, fmt.Errorf("unsupported BYOC image sync environment %q", environment)
	}
	workflow := strings.TrimSpace(byocConf.Workflow)
	if workflow == "" {
		return nil, fmt.Errorf("kernel image BYOC sync workflow is required")
	}
	return &client{
		github:      github.NewClient(httpClient),
		owner:       fidConf.RepositoryOwner,
		repo:        fidConf.RepositoryName,
		ref:         ref,
		environment: environment,
		workflow:    workflow,
	}, nil
}

func valueOrDefault(value, fallback string) string {
	if strings.TrimSpace(value) == "" {
		return fallback
	}
	return value
}

func (c *client) Dispatch(ctx context.Context, request DispatchRequest) (*WorkflowRun, error) {
	if c.github == nil {
		return nil, fmt.Errorf("BYOC image sync client is disabled")
	}
	workflow, err := c.workflowForEnvironment(request.Environment)
	if err != nil {
		return nil, err
	}
	if strings.TrimSpace(request.CorrelationID) == "" {
		return nil, fmt.Errorf("BYOC image sync correlation ID is required")
	}
	if len(request.Images) == 0 {
		return nil, fmt.Errorf("BYOC image sync images are required")
	}
	for _, image := range request.Images {
		if strings.TrimSpace(image.Source) == "" || strings.TrimSpace(image.Digest) == "" {
			return nil, fmt.Errorf("BYOC image source and digest are required")
		}
	}
	encodedImages, err := json.Marshal(request.Images)
	if err != nil {
		return nil, fmt.Errorf("marshal BYOC image sync manifest: %w", err)
	}

	c.dispatchMutex.Lock()
	defer c.dispatchMutex.Unlock()

	existing, maxRunID, err := c.findWorkflowRun(ctx, workflow, request, 0)
	if err != nil {
		return nil, err
	}
	if existing != nil && (existing.Status != "completed" || existing.Conclusion == "success") {
		return existing, nil
	}
	dispatchedAt := time.Now().UTC()
	_, err = c.github.Actions.CreateWorkflowDispatchEventByFileName(
		ctx,
		c.owner,
		c.repo,
		workflow,
		github.CreateWorkflowDispatchEventRequest{
			Ref: c.ref,
			Inputs: map[string]interface{}{
				"images":         string(encodedImages),
				"correlation-id": request.CorrelationID,
			},
		},
	)
	if err != nil {
		return nil, fmt.Errorf("dispatch BYOC image sync workflow: %w", err)
	}

	request.RequestedAt = dispatchedAt
	for {
		run, _, err := c.findWorkflowRun(ctx, workflow, request, maxRunID)
		if err != nil {
			return nil, err
		}
		if run != nil {
			return run, nil
		}
		select {
		case <-ctx.Done():
			return nil, ctx.Err()
		case <-time.After(workflowRunLookupInterval):
		}
	}
}

func (c *client) GetRun(ctx context.Context, runID int64) (*WorkflowRun, error) {
	if c.github == nil {
		return nil, fmt.Errorf("BYOC image sync client is disabled")
	}
	if runID <= 0 {
		return nil, fmt.Errorf("BYOC image sync workflow run ID must be positive")
	}
	run, _, err := c.github.Actions.GetWorkflowRunByID(ctx, c.owner, c.repo, runID)
	if err != nil {
		return nil, fmt.Errorf("get BYOC image sync workflow run: %w", err)
	}
	if run == nil {
		return nil, fmt.Errorf("get BYOC image sync workflow run: response is empty")
	}
	return workflowRunFromGithub(run), nil
}

func (c *client) workflowForEnvironment(environment string) (string, error) {
	requestedEnvironment := strings.ToLower(strings.TrimSpace(environment))
	if requestedEnvironment != c.environment {
		return "", fmt.Errorf(
			"BYOC image sync environment %q does not match configured environment %q",
			requestedEnvironment,
			c.environment,
		)
	}
	if strings.TrimSpace(c.ref) == "" || strings.TrimSpace(c.workflow) == "" {
		return "", fmt.Errorf("BYOC image sync workflow and ref are required for environment %q", environment)
	}
	return c.workflow, nil
}

func (c *client) findWorkflowRun(
	ctx context.Context,
	workflow string,
	request DispatchRequest,
	minRunID int64,
) (*WorkflowRun, int64, error) {
	runs, _, err := c.github.Actions.ListWorkflowRunsByFileName(
		ctx,
		c.owner,
		c.repo,
		workflow,
		&github.ListWorkflowRunsOptions{
			Branch: c.ref,
			Event:  "workflow_dispatch",
			ListOptions: github.ListOptions{
				PerPage: workflowRunLookupLimit,
			},
		},
	)
	if err != nil {
		return nil, 0, fmt.Errorf("find BYOC image sync workflow run: %w", err)
	}

	expectedTitle := fmt.Sprintf("BYOC %s sync %s", strings.ToLower(strings.TrimSpace(request.Environment)), request.CorrelationID)
	var maxSeenID int64
	var matched *WorkflowRun
	if runs != nil {
		for _, run := range runs.WorkflowRuns {
			if run == nil {
				continue
			}
			if run.GetID() > maxSeenID {
				maxSeenID = run.GetID()
			}
			if run.GetID() <= minRunID || run.GetDisplayTitle() != expectedTitle {
				continue
			}
			if run.CreatedAt != nil && run.CreatedAt.Time.Before(request.RequestedAt.Add(-time.Minute)) {
				continue
			}
			if matched == nil || run.GetID() > matched.ID {
				matched = workflowRunFromGithub(run)
			}
		}
	}
	return matched, maxSeenID, nil
}

func workflowRunFromGithub(run *github.WorkflowRun) *WorkflowRun {
	result := &WorkflowRun{
		ID:         run.GetID(),
		URL:        run.GetHTMLURL(),
		Status:     run.GetStatus(),
		Conclusion: run.GetConclusion(),
	}
	if run.CreatedAt != nil {
		result.CreatedAt = run.CreatedAt.Time
	}
	return result
}
