package automation

import (
	"context"
	"log/slog"
	"path"
	"sort"
	"strings"

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

type AutomationRepositoryAutomationQuery struct {
	RepositoryOwner     string
	RepositoryName      string
	DefinitionPath      string
	GitRef              string
	AuthorizationHeader string
}

type AutomationRepositoryAutomationValidateMutation struct {
	RepositoryOwner string
	RepositoryName  string
	DefinitionPath  string
	Content         string
}

type AutomationRepositoryAutomationSaveMutation struct {
	RepositoryOwner     string
	RepositoryName      string
	DefinitionPath      string
	Content             string
	Message             string
	Branch              string
	GitRef              string
	SHA                 string
	AuthorizationHeader string
}

type AutomationRepositoryAutomationDeleteMutation struct {
	RepositoryOwner     string
	RepositoryName      string
	DefinitionPath      string
	Message             string
	Branch              string
	GitRef              string
	SHA                 string
	AuthorizationHeader string
}

// MaxAutomationRepositorySourceCandidates caps merged automation repository list pages.
const MaxAutomationRepositorySourceCandidates = 1000

func (s *AutomationsService) ListAutomationRepositories(ctx context.Context, workspaceID string, subject string, authorizationHeader string, page int, perPage int) ([]AutomationSourceRepository, error) {
	if err := s.validateScope(workspaceID, subject); err != nil {
		return nil, err
	}
	if err := s.automationAuthorizer().CanReadAutomationRepo(ctx, s.automationAuthorizationRequest(ctx, workspaceID, subject, AutomationAuthorizationRequest{})); err != nil {
		return nil, err
	}
	page, perPage = normalizeAutomationPageWithMax(page, perPage, MaxAutomationRepositorySourceCandidates)
	control, err := s.sourceControlPlane()
	if err != nil {
		return nil, err
	}
	authorizationHeader = strings.TrimSpace(authorizationHeader)
	sourceScanWindow := automationRepositorySourceScanWindow(page, perPage)
	repos, err := listAutomationRepositorySourceWindow(func(sourcePage, sourcePerPage int) ([]AutomationSourceRepository, error) {
		return control.ListAutomationRepositories(ctx, authorizationHeader, sourcePage, sourcePerPage)
	}, sourceScanWindow)
	if err != nil {
		return nil, err
	}
	workspaceOwner := expectedWorkspaceRepositoryOwnerKey(workspaceID)
	if workspaceOwner != "" {
		for _, org := range expectedAutomationRepositoryOrgs(workspaceOwner) {
			orgRepos, err := listAutomationRepositorySourceWindow(func(sourcePage, sourcePerPage int) ([]AutomationSourceRepository, error) {
				return control.ListAutomationOrgRepositories(ctx, authorizationHeader, org, sourcePage, sourcePerPage)
			}, sourceScanWindow)
			if err != nil {
				return nil, err
			}
			repos = append(repos, orgRepos...)
		}
	}
	merged, err := s.mergePrimaryAutomationRepositories(ctx, workspaceID, repos, sourceScanWindow)
	if err != nil {
		return nil, err
	}
	return automationRepositoryPage(merged, page, perPage), nil
}

func expectedWorkspaceRepositoryOwnerKey(workspaceID string) string {
	return strings.TrimSpace(workspaceID)
}

func expectedAutomationRepositoryOrgs(workspaceOwner string) []string {
	workspaceOwner = expectedWorkspaceRepositoryOwnerKey(workspaceOwner)
	if workspaceOwner == "" {
		return nil
	}
	return []string{workspaceOwner}
}

func (s *AutomationsService) mergePrimaryAutomationRepositories(ctx context.Context, workspaceID string, repos []AutomationSourceRepository, sourceScanWindow int) ([]AutomationSourceRepository, error) {
	merged := make([]AutomationSourceRepository, 0, len(repos)+8)
	seen := map[string]struct{}{}
	add := func(repo AutomationSourceRepository) {
		repo.Owner = strings.TrimSpace(repo.Owner)
		repo.Name = strings.TrimSpace(repo.Name)
		repo.FullName = strings.TrimSpace(repo.FullName)
		if repo.FullName == "" && repo.Owner != "" && repo.Name != "" {
			repo.FullName = repo.Owner + "/" + repo.Name
		}
		if repo.Owner == "" && strings.Contains(repo.FullName, "/") {
			repo.Owner = strings.SplitN(repo.FullName, "/", 2)[0]
		}
		if repo.Name == "" && strings.Contains(repo.FullName, "/") {
			repo.Name = strings.SplitN(repo.FullName, "/", 2)[1]
		}
		if repo.Owner == "" || repo.Name == "" || repo.FullName == "" {
			return
		}
		key := strings.ToLower(repo.FullName)
		if _, ok := seen[key]; ok {
			return
		}
		seen[key] = struct{}{}
		merged = append(merged, repo)
	}
	for _, repo := range repos {
		add(repo)
	}
	sort.SliceStable(merged, func(i, j int) bool {
		return strings.ToLower(merged[i].FullName) < strings.ToLower(merged[j].FullName)
	})
	return merged, nil
}

func automationRepositorySourceScanWindow(page int, perPage int) int {
	page, perPage = normalizeAutomationPageWithMax(page, perPage, MaxAutomationRepositorySourceCandidates)
	if page > MaxAutomationRepositorySourceCandidates/perPage {
		return MaxAutomationRepositorySourceCandidates
	}
	window := page*perPage + perPage
	if window > MaxAutomationRepositorySourceCandidates {
		return MaxAutomationRepositorySourceCandidates
	}
	return window
}

// listAutomationRepositorySourceWindow reads only a bounded source window for the
// requested merged page. The extra page of source candidates tolerates local
// filtering and deduplication without scanning every workspace repository.
// Ordering remains deterministic, but it is not an exact global ordering proof
// across AGS, project, and KB sources.
func listAutomationRepositorySourceWindow(list func(page int, perPage int) ([]AutomationSourceRepository, error), sourceScanWindow int) ([]AutomationSourceRepository, error) {
	if sourceScanWindow <= 0 {
		return nil, nil
	}
	const sourcePerPage = 100
	pages := (sourceScanWindow + sourcePerPage - 1) / sourcePerPage
	repos := make([]AutomationSourceRepository, 0, sourceScanWindow)
	for page := 1; page <= pages; page++ {
		batch, err := list(page, sourcePerPage)
		if err != nil {
			return nil, err
		}
		repos = append(repos, batch...)
		if len(batch) < sourcePerPage {
			break
		}
	}
	if len(repos) > sourceScanWindow {
		repos = repos[:sourceScanWindow]
	}
	return repos, nil
}

func automationRepositoryPage(repos []AutomationSourceRepository, page int, perPage int) []AutomationSourceRepository {
	page, perPage = normalizeAutomationPageWithMax(page, perPage, MaxAutomationRepositorySourceCandidates)
	if len(repos) == 0 {
		return []AutomationSourceRepository{}
	}
	maxPage := (len(repos) + perPage - 1) / perPage
	if page > maxPage {
		return []AutomationSourceRepository{}
	}
	start := (page - 1) * perPage
	end := start + perPage
	if end > len(repos) {
		end = len(repos)
	}
	return repos[start:end]
}

func workspaceKBAutomationRepositoryOwner(workspaceID string) string {
	workspaceOwner := expectedWorkspaceRepositoryOwnerKey(workspaceID)
	if workspaceOwner == "" {
		return ""
	}
	return workspaceOwner
}

func (s *AutomationsService) ListRepositoryAutomationFiles(ctx context.Context, workspaceID string, subject string, input AutomationRepositoryAutomationQuery) ([]AutomationSourceFile, error) {
	if err := s.validateScope(workspaceID, subject); err != nil {
		return nil, err
	}
	input = normalizeAutomationRepositoryQuery(input)
	if err := validateAutomationRepository(input.RepositoryOwner, input.RepositoryName); err != nil {
		return nil, err
	}
	if err := s.automationAuthorizer().CanReadAutomationRepo(ctx, s.automationAuthorizationRequest(ctx, workspaceID, subject, AutomationAuthorizationRequest{RepositoryOwner: input.RepositoryOwner, RepositoryName: input.RepositoryName})); err != nil {
		return nil, err
	}
	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.GitRef, AuthorizationHeader: input.AuthorizationHeader})
	if err != nil {
		return nil, err
	}
	if err := s.bindAutomationRepository(ctx, workspaceID, subject, input.RepositoryOwner, input.RepositoryName, input.GitRef); err != nil {
		return nil, err
	}
	return files, nil
}

func (s *AutomationsService) GetRepositoryAutomationFile(ctx context.Context, workspaceID string, subject string, input AutomationRepositoryAutomationQuery) (AutomationSourceContent, error) {
	if err := s.validateScope(workspaceID, subject); err != nil {
		return AutomationSourceContent{}, err
	}
	input = normalizeAutomationRepositoryQuery(input)
	if err := validateAutomationRepository(input.RepositoryOwner, input.RepositoryName); err != nil {
		return AutomationSourceContent{}, err
	}
	if !isAutomationSourcePath(input.DefinitionPath) {
		return AutomationSourceContent{}, invalidArgument("Automation definition path must be under .agentteam and end in .yml or .yaml")
	}
	if err := s.automationAuthorizer().CanReadAutomationRepo(ctx, s.automationAuthorizationRequest(ctx, workspaceID, subject, AutomationAuthorizationRequest{RepositoryOwner: input.RepositoryOwner, RepositoryName: input.RepositoryName})); err != nil {
		return AutomationSourceContent{}, err
	}
	control, err := s.sourceControlPlane()
	if err != nil {
		return AutomationSourceContent{}, err
	}
	content, err := control.GetAutomationFile(ctx, AutomationSourceRequest{WorkspaceID: workspaceID, RepositoryOwner: input.RepositoryOwner, RepositoryName: input.RepositoryName, DefinitionPath: input.DefinitionPath, GitRef: input.GitRef, AuthorizationHeader: input.AuthorizationHeader})
	if err != nil {
		return AutomationSourceContent{}, err
	}
	if err := s.bindAutomationRepository(ctx, workspaceID, subject, input.RepositoryOwner, input.RepositoryName, firstNonEmptyString(content.Ref, input.GitRef)); err != nil {
		return AutomationSourceContent{}, err
	}
	return content, nil
}

func (s *AutomationsService) ValidateRepositoryAutomationContent(ctx context.Context, workspaceID string, subject string, input AutomationRepositoryAutomationValidateMutation) (dsl.ValidationReport, error) {
	if err := s.validateScope(workspaceID, subject); err != nil {
		return dsl.ValidationReport{}, err
	}
	input.RepositoryOwner = strings.TrimSpace(input.RepositoryOwner)
	input.RepositoryName = strings.TrimSpace(input.RepositoryName)
	if err := s.automationAuthorizer().CanReadAutomationRepo(ctx, s.automationAuthorizationRequest(ctx, workspaceID, subject, AutomationAuthorizationRequest{RepositoryOwner: input.RepositoryOwner, RepositoryName: input.RepositoryName})); err != nil {
		return dsl.ValidationReport{}, err
	}
	return s.validateRepositoryAutomationContent(input), nil
}

func (s *AutomationsService) SaveRepositoryAutomationFile(ctx context.Context, workspaceID string, subject string, input AutomationRepositoryAutomationSaveMutation) (AutomationSourceWriteResult, error) {
	if err := s.validateScope(workspaceID, subject); err != nil {
		return AutomationSourceWriteResult{}, err
	}
	input = normalizeAutomationSaveInput(input)
	if err := validateAutomationSaveInput(input); err != nil {
		return AutomationSourceWriteResult{}, err
	}
	input.DefinitionPath = normalizeAutomationSourcePath(input.DefinitionPath)
	if err := s.automationAuthorizer().CanWriteAutomationRepo(ctx, s.automationAuthorizationRequest(ctx, workspaceID, subject, AutomationAuthorizationRequest{RepositoryOwner: input.RepositoryOwner, RepositoryName: input.RepositoryName})); err != nil {
		return AutomationSourceWriteResult{}, err
	}
	report := s.validateRepositoryAutomationContent(AutomationRepositoryAutomationValidateMutation{
		RepositoryOwner: input.RepositoryOwner,
		RepositoryName:  input.RepositoryName,
		DefinitionPath:  input.DefinitionPath,
		Content:         input.Content,
	})
	if !report.Valid {
		return AutomationSourceWriteResult{}, automationValidationWriteError(report)
	}
	control, err := s.sourceWriteControlPlane()
	if err != nil {
		return AutomationSourceWriteResult{}, err
	}
	result, err := control.PutAutomationFile(ctx, AutomationSourceWriteRequest{
		WorkspaceID:         workspaceID,
		Actor:               subject,
		RepositoryOwner:     input.RepositoryOwner,
		RepositoryName:      input.RepositoryName,
		DefinitionPath:      input.DefinitionPath,
		Branch:              input.Branch,
		Content:             input.Content,
		Message:             input.Message,
		SHA:                 input.SHA,
		AuthorizationHeader: input.AuthorizationHeader,
	})
	if err != nil {
		slog.WarnContext(ctx, "automation source save failed", "workspace_id", strings.TrimSpace(workspaceID), "repository", input.RepositoryOwner+"/"+input.RepositoryName, "definition_path", input.DefinitionPath, "error", err)
		return AutomationSourceWriteResult{}, err
	}
	if err := s.bindAutomationRepository(ctx, workspaceID, subject, input.RepositoryOwner, input.RepositoryName, firstNonEmptyString(result.Content.Ref, input.Branch, input.GitRef)); err != nil {
		return AutomationSourceWriteResult{}, err
	}
	return result, nil
}

func (s *AutomationsService) DeleteRepositoryAutomationFile(ctx context.Context, workspaceID string, subject string, input AutomationRepositoryAutomationDeleteMutation) error {
	if err := s.validateScope(workspaceID, subject); err != nil {
		return err
	}
	input = normalizeAutomationDeleteInput(input)
	if err := validateAutomationDeleteInput(input); err != nil {
		return err
	}
	input.DefinitionPath = normalizeAutomationSourcePath(input.DefinitionPath)
	if err := s.automationAuthorizer().CanWriteAutomationRepo(ctx, s.automationAuthorizationRequest(ctx, workspaceID, subject, AutomationAuthorizationRequest{RepositoryOwner: input.RepositoryOwner, RepositoryName: input.RepositoryName})); err != nil {
		return err
	}
	control, err := s.sourceWriteControlPlane()
	if err != nil {
		return err
	}
	if input.Message == "" {
		input.Message = "Delete " + input.DefinitionPath
	}
	if err := control.DeleteAutomationFile(ctx, AutomationSourceDeleteRequest{
		WorkspaceID:         workspaceID,
		Actor:               subject,
		RepositoryOwner:     input.RepositoryOwner,
		RepositoryName:      input.RepositoryName,
		DefinitionPath:      input.DefinitionPath,
		Branch:              input.Branch,
		Message:             input.Message,
		SHA:                 input.SHA,
		AuthorizationHeader: input.AuthorizationHeader,
	}); err != nil {
		slog.WarnContext(ctx, "automation source delete failed", "workspace_id", strings.TrimSpace(workspaceID), "repository", input.RepositoryOwner+"/"+input.RepositoryName, "definition_path", input.DefinitionPath, "error", err)
		return err
	}
	return nil
}

func (s *AutomationsService) validateRepositoryAutomationContent(input AutomationRepositoryAutomationValidateMutation) dsl.ValidationReport {
	if err := validateAutomationRepository(input.RepositoryOwner, input.RepositoryName); err != nil {
		return dsl.ValidationReport{Valid: false, Errors: []dsl.ValidationError{{Code: "automation_repository_invalid", Path: "repository", Message: err.Error()}}}
	}
	if !isAutomationSourcePath(input.DefinitionPath) {
		return dsl.ValidationReport{Valid: false, Errors: []dsl.ValidationError{{Code: "definition_path_invalid", Path: "definition_path", Message: "automation definition path must be under .agentteam and end in .yml or .yaml"}}}
	}
	limits := s.automationLimits()
	if len([]byte(input.Content)) > limits.MaxAutomationSourceBytes {
		return dsl.ValidationReport{Valid: false, Errors: []dsl.ValidationError{{Code: "automation_source_limit_exceeded", Path: "content", Message: "automation source exceeds maximum size"}}}
	}
	_, report := s.decodeAndValidateAutomationContent(input.Content, input.DefinitionPath)
	return report
}

func (s *AutomationsService) sourceControlPlane() (AutomationSourceControlPlane, error) {
	if s == nil || s.sourceResolver == nil {
		return nil, invalidArgument("Automation source resolver is not configured")
	}
	control, ok := s.sourceResolver.(AutomationSourceControlPlane)
	if !ok {
		return nil, invalidArgument("Automation source control plane is not configured")
	}
	return control, nil
}

func (s *AutomationsService) sourceWriteControlPlane() (AutomationSourceWriteControlPlane, error) {
	if s == nil || s.sourceResolver == nil {
		return nil, invalidArgument("Automation source resolver is not configured")
	}
	control, ok := s.sourceResolver.(AutomationSourceWriteControlPlane)
	if !ok {
		return nil, invalidArgument("Automation source write control plane is not configured")
	}
	return control, nil
}

func normalizeAutomationRepositoryQuery(input AutomationRepositoryAutomationQuery) AutomationRepositoryAutomationQuery {
	input.RepositoryOwner = strings.TrimSpace(input.RepositoryOwner)
	input.RepositoryName = strings.TrimSpace(input.RepositoryName)
	input.DefinitionPath = strings.TrimSpace(input.DefinitionPath)
	input.GitRef = strings.TrimSpace(input.GitRef)
	input.AuthorizationHeader = strings.TrimSpace(input.AuthorizationHeader)
	return input
}

func normalizeAutomationSaveInput(input AutomationRepositoryAutomationSaveMutation) AutomationRepositoryAutomationSaveMutation {
	input.RepositoryOwner = strings.TrimSpace(input.RepositoryOwner)
	input.RepositoryName = strings.TrimSpace(input.RepositoryName)
	input.DefinitionPath = strings.TrimSpace(input.DefinitionPath)
	input.Message = strings.TrimSpace(input.Message)
	input.SHA = strings.TrimSpace(input.SHA)
	input.AuthorizationHeader = strings.TrimSpace(input.AuthorizationHeader)
	input.Branch = normalizeAutomationWriteBranch(input.Branch)
	input.GitRef = normalizeAutomationWriteBranch(input.GitRef)
	if input.Branch == "" {
		input.Branch = input.GitRef
	}
	return input
}

func normalizeAutomationDeleteInput(input AutomationRepositoryAutomationDeleteMutation) AutomationRepositoryAutomationDeleteMutation {
	input.RepositoryOwner = strings.TrimSpace(input.RepositoryOwner)
	input.RepositoryName = strings.TrimSpace(input.RepositoryName)
	input.DefinitionPath = strings.TrimSpace(input.DefinitionPath)
	input.Message = strings.TrimSpace(input.Message)
	input.SHA = strings.TrimSpace(input.SHA)
	input.AuthorizationHeader = strings.TrimSpace(input.AuthorizationHeader)
	input.Branch = normalizeAutomationWriteBranch(input.Branch)
	input.GitRef = normalizeAutomationWriteBranch(input.GitRef)
	if input.Branch == "" {
		input.Branch = input.GitRef
	}
	return input
}

func normalizeAutomationSourcePath(value string) string {
	return strings.TrimPrefix(path.Clean("/"+strings.TrimSpace(value)), "/")
}

func validateAutomationRepository(owner string, repo string) error {
	if strings.TrimSpace(owner) == "" || strings.TrimSpace(repo) == "" {
		return invalidArgument("Automation repository owner and name are required")
	}
	return nil
}

func isAutomationSourcePath(value string) bool {
	if definitionPathHasTraversal(value) {
		return false
	}
	cleaned := normalizeAutomationSourcePath(value)
	if !strings.HasPrefix(cleaned, ".agentteam/") {
		return false
	}
	lower := strings.ToLower(cleaned)
	return strings.HasSuffix(lower, ".yml") || strings.HasSuffix(lower, ".yaml")
}

func validateAutomationSaveInput(input AutomationRepositoryAutomationSaveMutation) error {
	if err := validateAutomationRepository(input.RepositoryOwner, input.RepositoryName); err != nil {
		return err
	}
	if input.Content == "" {
		return invalidArgument("Automation source content is required")
	}
	if !isAutomationSourcePath(input.DefinitionPath) {
		return invalidArgument("Automation definition path must be under .agentteam and end in .yml or .yaml")
	}
	if input.Branch != "" && input.GitRef != "" && input.Branch != input.GitRef {
		return invalidArgument("Automation branch and ref must refer to the same branch")
	}
	if automationWriteBranchInvalid(input.Branch) || automationWriteBranchInvalid(input.GitRef) {
		return invalidArgument("Automation branch must be a branch name")
	}
	return nil
}

func validateAutomationDeleteInput(input AutomationRepositoryAutomationDeleteMutation) error {
	if err := validateAutomationRepository(input.RepositoryOwner, input.RepositoryName); err != nil {
		return err
	}
	if !isAutomationSourcePath(input.DefinitionPath) {
		return invalidArgument("Automation definition path must be under .agentteam and end in .yml or .yaml")
	}
	if input.SHA == "" {
		return invalidArgument("Automation source sha is required")
	}
	if input.Branch != "" && input.GitRef != "" && input.Branch != input.GitRef {
		return invalidArgument("Automation branch and ref must refer to the same branch")
	}
	if automationWriteBranchInvalid(input.Branch) || automationWriteBranchInvalid(input.GitRef) {
		return invalidArgument("Automation branch must be a branch name")
	}
	return nil
}

func normalizeAutomationWriteBranch(value string) string {
	value = strings.TrimSpace(value)
	value = strings.TrimPrefix(value, "refs/heads/")
	value = strings.TrimPrefix(value, "heads/")
	return strings.TrimSpace(value)
}

func automationWriteBranchInvalid(value string) bool {
	value = strings.TrimSpace(value)
	return strings.HasPrefix(value, "refs/") || isCommitSHA(value)
}

func definitionPathHasTraversal(value string) bool {
	normalized := strings.ReplaceAll(strings.TrimSpace(value), "\\", "/")
	for _, part := range strings.Split(normalized, "/") {
		if part == ".." {
			return true
		}
	}
	return false
}

func automationValidationWriteError(report dsl.ValidationReport) error {
	if len(report.Errors) == 0 {
		return invalidArgument("Automation spec is invalid")
	}
	first := report.Errors[0]
	switch first.Code {
	case "invalid_yaml":
		return invalidArgument("Automation source must be valid AgentTeam Automation YAML or normalized JSON")
	case "definition_path_invalid":
		return invalidArgument("Automation definition path must be under .agentteam and end in .yml or .yaml")
	case "automation_repository_invalid":
		return invalidArgument(first.Message)
	case "automation_source_limit_exceeded":
		return invalidArgument("Automation source exceeds maximum size")
	default:
		if strings.TrimSpace(first.Message) == "" {
			return invalidArgument("Automation spec is invalid")
		}
		return invalidArgument("Automation spec is invalid: " + first.Message)
	}
}
