package azure

import (
	"context"
	"encoding/base64"
	"errors"
	"fmt"

	"freeleaps.com/gitops/initializer/internal/repo"
	"github.com/microsoft/azure-devops-go-api/azuredevops"
	"github.com/microsoft/azure-devops-go-api/azuredevops/git"
)

type AzureDevOpsRepoManager struct {
	options *repo.Options
	client  git.Client
}

var _ repo.RepoManager = &AzureDevOpsRepoManager{}

func init() {
	repo.RegisterPlugin("azure-devops", func() repo.RepoManager {
		return &AzureDevOpsRepoManager{}
	})
}

// Initialize implements repo.RepoManager.
func (a *AzureDevOpsRepoManager) Initialize(context context.Context, opts ...repo.Option) error {
	options := &repo.Options{}
	for _, opt := range opts {
		opt(options)
	}

	if options.Project == "" {
		return errors.New("missing project name")
	}

	if options.Name == "" {
		return errors.New("missing repository name")
	}

	if options.Branch == "" {
		options.Branch = "master"
	}

	if options.AuthorName == "" {
		options.AuthorName = repo.DefaultAuthorName
	}

	if options.AuthorEmail == "" {
		options.AuthorEmail = repo.DefaultAuthorEmail
	}

	if options.PluginConfig == nil {
		return errors.New("missing plugin config")
	}

	config := options.PluginConfig

	pat, err := config.Get("pat")
	if err != nil {
		return errors.New("missing PAT")
	}

	organization, err := config.Get("organizationUrl")
	if err != nil {
		return errors.New("missing organization URL")
	}

	conn := azuredevops.NewPatConnection(organization.(string), pat.(string))
	a.client, err = git.NewClient(context, conn)
	if err != nil {
		return fmt.Errorf("failed to create git client: %w", err)
	}
	a.options = options
	return nil
}

func (a *AzureDevOpsRepoManager) getBranchHeadCommit() (string, error) {
	filter := "heads/"
	filterContains := a.options.Branch

	refs, err := a.client.GetRefs(context.Background(), git.GetRefsArgs{
		RepositoryId:   &a.options.Name,
		Project:        &a.options.Project,
		Filter:         &filter,
		FilterContains: &filterContains,
	})

	if err != nil {
		return "", fmt.Errorf("failed to get branch ref: %v", err)
	}

	if len(refs.Value) == 0 || (refs.Value)[0].ObjectId == nil {
		return "", fmt.Errorf("branch '%s' not found", a.options.Branch)
	}

	return *refs.Value[0].ObjectId, nil
}

// ApplyChanges implements repo.RepoManager.
func (a *AzureDevOpsRepoManager) ApplyChanges(context context.Context, changes []repo.ChangeRequest, message string) error {
	targetRepo, err := a.client.GetRepository(context, git.GetRepositoryArgs{
		RepositoryId: &a.options.Name,
		Project:      &a.options.Project,
	})
	if err != nil {
		return fmt.Errorf("failed to get repository: %w", err)
	}

	// convert changes to git changes
	gitChanges := make([]interface{}, 0, len(changes))
	for _, change := range changes {
		gitChange := git.GitChange{
			ChangeType: toAzureGitChangeType(change.Operation),
			Item: &git.GitItem{
				Path: &change.Path,
			},
		}

		if change.Operation != repo.OpDelete {
			contentBase64 := base64.StdEncoding.EncodeToString(change.Content)
			gitChange.NewContent = &git.ItemContent{
				Content:     &contentBase64,
				ContentType: &git.ItemContentTypeValues.Base64Encoded,
			}
		}

		gitChanges = append(gitChanges, gitChange)
	}

	// get head commit id
	baseCommitId, err := a.getBranchHeadCommit()
	if err != nil {
		return err
	}

	// generate ref name
	refName := "refs/heads/" + a.options.Branch

	// create push
	_, err = a.client.CreatePush(context, git.CreatePushArgs{
		RepositoryId: &a.options.Name,
		Project:      targetRepo.Project.Name,
		Push: &git.GitPush{
			RefUpdates: &[]git.GitRefUpdate{
				{
					Name:        &refName,
					OldObjectId: &baseCommitId,
				},
			},
			Commits: &[]git.GitCommitRef{
				{
					Comment: &message,
					Changes: &gitChanges,
					Author: &git.GitUserDate{
						Name:  &a.options.AuthorName,
						Email: &a.options.AuthorEmail,
					},
				},
			},
		},
	})

	return err
}

func toAzureGitChangeType(op repo.FileOperation) *git.VersionControlChangeType {
	switch op {
	case repo.OpCreate:
		return &git.VersionControlChangeTypeValues.Add
	case repo.OpDelete:
		return &git.VersionControlChangeTypeValues.Delete
	case repo.OpUpdate:
		return &git.VersionControlChangeTypeValues.Edit
	default:
		panic("unreachable")
	}
}
