package automation

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"sort"
	"strings"

	"github.com/tidbcloud/AgentTeam/internal/automation/dsl"
	automationstore "github.com/tidbcloud/AgentTeam/internal/store/automation"
)

// AutomationEventerPrincipalID identifies repository-triggered automation runs.
const AutomationEventerPrincipalID = "agentteam-automation-eventer"

// AutomationRepositoryPushEventMutation carries a normalized repository branch
// update event after upstream verification/routing has already happened.
type AutomationRepositoryPushEventMutation struct {
	RepositoryOwner     string
	RepositoryName      string
	AuthorizationHeader string
	EventID             string
	DeliveryKey         string
	Ref                 string
	Before              string
	After               string
	ChangedPaths        []string
	ActorRef            string
	ActorDisplay        string
	Sender              string
	EventPayloadJSON    string
}

// StartRepositoryAutomationRunsForPushEvent creates automation runs for every
// automation file at the updated commit whose on.push trigger matches the
// normalized repository update.
func (s *AutomationsService) StartRepositoryAutomationRunsForPushEvent(ctx context.Context, workspaceID string, subject string, input AutomationRepositoryPushEventMutation) ([]automationstore.AutomationRun, error) {
	if err := s.validateScope(workspaceID, subject); err != nil {
		return nil, err
	}
	input = normalizeRepositoryPushEventInput(input)
	limits := s.automationLimits()
	if err := validateRepositoryPushEventInput(input, limits); err != nil {
		return nil, err
	}
	if err := s.automationAuthorizer().CanReceiveAutomationEvent(ctx, s.automationAuthorizationRequest(ctx, workspaceID, subject, AutomationAuthorizationRequest{RepositoryOwner: input.RepositoryOwner, RepositoryName: input.RepositoryName})); err != nil {
		return nil, err
	}
	if isZeroGitSHA(input.After) {
		return []automationstore.AutomationRun{}, nil
	}
	control, err := s.sourceControlPlane()
	if err != nil {
		return nil, err
	}
	files, err := control.ListAutomationFiles(ctx, AutomationSourceRequest{
		WorkspaceID:         workspaceID,
		RepositoryOwner:     input.RepositoryOwner,
		RepositoryName:      input.RepositoryName,
		GitRef:              input.After,
		AuthorizationHeader: input.AuthorizationHeader,
	})
	if err != nil {
		return nil, fmt.Errorf("list automation files for push event: %w", err)
	}
	sort.SliceStable(files, func(i, j int) bool {
		return files[i].Path < files[j].Path
	})
	event := dsl.PushEvent{
		Ref:               input.Ref,
		ChangedPaths:      input.ChangedPaths,
		ChangedPathsKnown: input.ChangedPaths != nil,
	}
	trigger := pushAutomationStartTrigger(event)
	runs := make([]automationstore.AutomationRun, 0, len(files))
	for _, file := range files {
		definitionPath := normalizeAutomationSourcePath(file.Path)
		if !isAutomationSourcePath(definitionPath) {
			continue
		}
		runInput := automationRepositoryPushEventRunInput(workspaceID, input, definitionPath)
		run, err := s.startRepositoryAutomationRunFromSnapshot(ctx, workspaceID, AutomationEventerPrincipalID, runInput, trigger)
		if errors.Is(err, errAutomationTriggerNotMatched) {
			continue
		}
		if err != nil {
			return nil, err
		}
		runs = append(runs, run)
	}
	return runs, nil
}

func normalizeRepositoryPushEventInput(input AutomationRepositoryPushEventMutation) AutomationRepositoryPushEventMutation {
	input.RepositoryOwner = strings.TrimSpace(input.RepositoryOwner)
	input.RepositoryName = strings.TrimSpace(input.RepositoryName)
	input.AuthorizationHeader = strings.TrimSpace(input.AuthorizationHeader)
	input.EventID = strings.TrimSpace(input.EventID)
	input.DeliveryKey = strings.TrimSpace(input.DeliveryKey)
	input.Ref = strings.TrimSpace(input.Ref)
	input.Before = strings.TrimSpace(input.Before)
	input.After = strings.TrimSpace(input.After)
	input.ActorRef = strings.TrimSpace(input.ActorRef)
	input.ActorDisplay = strings.TrimSpace(input.ActorDisplay)
	input.Sender = strings.TrimSpace(input.Sender)
	input.EventPayloadJSON = strings.TrimSpace(input.EventPayloadJSON)
	if input.EventID == "" {
		input.EventID = input.DeliveryKey
	}
	if input.ActorRef == "" {
		input.ActorRef = input.Sender
	}
	if input.ActorDisplay == "" {
		input.ActorDisplay = input.ActorRef
	}
	if input.ChangedPaths != nil {
		paths := make([]string, 0, len(input.ChangedPaths))
		seen := map[string]struct{}{}
		for _, changedPath := range input.ChangedPaths {
			changedPath = normalizeAutomationSourcePath(changedPath)
			if changedPath == "." || changedPath == "" {
				continue
			}
			if _, ok := seen[changedPath]; ok {
				continue
			}
			seen[changedPath] = struct{}{}
			paths = append(paths, changedPath)
		}
		input.ChangedPaths = paths
	}
	return input
}

func pushAutomationStartTrigger(event dsl.PushEvent) automationStartTrigger {
	return func(on any) error {
		if dsl.MatchesPushTrigger(on, event) {
			return nil
		}
		return errAutomationTriggerNotMatched
	}
}

func validateRepositoryPushEventInput(input AutomationRepositoryPushEventMutation, limits AutomationLimits) error {
	if err := validateAutomationRepository(input.RepositoryOwner, input.RepositoryName); err != nil {
		return err
	}
	if automationRepositoryPushEventIdentity(input) == "" {
		return invalidArgument("Automation push event_id or delivery_key is required")
	}
	if input.Ref == "" {
		return invalidArgument("Automation push ref is required")
	}
	if !isZeroGitSHA(input.After) && !isCommitSHA(input.After) {
		return invalidArgument("Automation push after must be an immutable commit SHA")
	}
	if len([]byte(input.EventPayloadJSON)) > normalizeAutomationLimits(limits).MaxRunInputBytes {
		return invalidArgument("Automation push event_payload exceeds maximum size")
	}
	if err := validateOptionalJSON(input.EventPayloadJSON, "Automation push event_payload must be valid JSON"); err != nil {
		return err
	}
	return nil
}

func automationRepositoryPushEventRunInput(workspaceID string, event AutomationRepositoryPushEventMutation, definitionPath string) AutomationRepositoryRunMutation {
	targetRef := event.RepositoryOwner + "/" + event.RepositoryName + "@" + event.Ref + "#" + event.After
	return AutomationRepositoryRunMutation{
		RepositoryOwner:     event.RepositoryOwner,
		RepositoryName:      event.RepositoryName,
		DefinitionPath:      definitionPath,
		GitRef:              event.After,
		Branch:              automationBranchFromRef(event.Ref),
		BeforeSHA:           event.Before,
		ActorRef:            event.ActorRef,
		ActorDisplay:        event.ActorDisplay,
		AuthorizationHeader: event.AuthorizationHeader,
		IdempotencyKey:      automationRepositoryPushRunIdempotencyKey(workspaceID, event, definitionPath),
		InputJSON:           event.EventPayloadJSON,
		TriggerKind:         "push",
		TriggerRef:          automationRepositoryPushEventIdentity(event),
		TargetKind:          "repository_ref",
		TargetRef:           targetRef,
		TriggerContextJSON:  automationPushTriggerContextJSON(event),
	}
}

func automationRepositoryPushRunIdempotencyKey(workspaceID string, event AutomationRepositoryPushEventMutation, definitionPath string) string {
	hash := sha256.New()
	eventIdentity := strings.TrimSpace(event.DeliveryKey)
	if eventIdentity == "" {
		eventIdentity = automationRepositoryPushEventIdentity(event)
	}
	parts := []string{
		strings.TrimSpace(workspaceID),
		event.RepositoryOwner + "/" + event.RepositoryName,
		eventIdentity,
		event.Ref,
		event.After,
		definitionPath,
	}
	for _, part := range parts {
		hash.Write([]byte(strings.TrimSpace(part)))
		hash.Write([]byte{0})
	}
	return "push:" + hex.EncodeToString(hash.Sum(nil))
}

func automationRepositoryPushEventIdentity(event AutomationRepositoryPushEventMutation) string {
	if eventID := strings.TrimSpace(event.EventID); eventID != "" {
		return eventID
	}
	return strings.TrimSpace(event.DeliveryKey)
}

func isZeroGitSHA(value string) bool {
	value = strings.TrimSpace(value)
	if value == "" {
		return true
	}
	for _, char := range value {
		if char != '0' {
			return false
		}
	}
	return true
}
