package github

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"strings"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/validation/field"

	provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
	"github.com/grafana/grafana/apps/provisioning/pkg/repository"
	"github.com/grafana/grafana/apps/provisioning/pkg/repository/git"
	"github.com/grafana/grafana/apps/provisioning/pkg/safepath"
	common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
)

// Make sure all public functions of this struct call the (*githubRepository).logger function, to ensure the GH repo details are included.
type githubRepository struct {
	git.GitRepository
	config *provisioning.Repository
	gh     Client // assumes github.com base URL

	owner string
	repo  string
}

// GithubRepository is an interface that combines all repository capabilities
// needed for GitHub repositories.

//go:generate mockery --name GithubRepository --structname MockGithubRepository --inpackage --filename github_repository_mock.go --with-expecter
type GithubRepository interface {
	repository.Repository
	repository.Versioned
	repository.Writer
	repository.Reader
	repository.RepositoryWithURLs
	repository.StageableRepository
	repository.BranchHandler
	Owner() string
	Repo() string
	Client() Client
}

func NewRepository(
	ctx context.Context,
	config *provisioning.Repository,
	gitRepo git.GitRepository,
	factory *Factory,
	token common.RawSecureValue,
) (GithubRepository, error) {
	owner, repo, err := ParseOwnerRepoGithub(config.Spec.GitHub.URL)
	if err != nil {
		return nil, fmt.Errorf("parse owner and repo: %w", err)
	}

	return &githubRepository{
		config:        config,
		GitRepository: gitRepo,
		gh:            factory.New(ctx, token), // TODO, baseURL from config
		owner:         owner,
		repo:          repo,
	}, nil
}

func (r *githubRepository) Owner() string {
	return r.owner
}

func (r *githubRepository) Repo() string {
	return r.repo
}

func (r *githubRepository) Client() Client {
	return r.gh
}

func (r *githubRepository) GetDefaultBranch(ctx context.Context) (string, error) {
	repo, err := r.gh.GetRepository(ctx, r.owner, r.repo)
	if err != nil {
		return "", fmt.Errorf("failed to get repository metadata: %w", err)
	}
	return repo.DefaultBranch, nil
}

func (r *githubRepository) GetCurrentBranch() string {
	return r.config.Spec.GitHub.Branch
}

func (r *githubRepository) SetBranch(branch string) {
	r.config.Spec.GitHub.Branch = branch
	r.GitRepository.SetBranch(branch)
}

func ParseOwnerRepoGithub(giturl string) (owner string, repo string, err error) {
	giturl = strings.TrimSuffix(giturl, ".git")
	giturl = strings.TrimSuffix(giturl, "/")

	parsed, e := url.Parse(giturl)
	if e != nil {
		err = e
		return
	}
	parts := strings.Split(parsed.Path, "/")
	if len(parts) < 3 {
		err = fmt.Errorf("unable to parse repo+owner from url")
		return
	}
	return parts[1], parts[2], nil
}

// Test implements provisioning.Repository.
func (r *githubRepository) Test(ctx context.Context) (*provisioning.TestResults, error) {
	url := r.config.Spec.GitHub.URL
	_, _, err := ParseOwnerRepoGithub(url)
	if err != nil {
		return repository.FromFieldError(field.Invalid(
			field.NewPath("spec", "github", "url"), url, err.Error())), nil
	}

	// For Github repositories, in case the branch is empty, we get the default branch and set it up for testing.
	if r.GetCurrentBranch() == "" {
		branch, err := r.GetDefaultBranch(ctx)
		if err != nil {
			return nil, err
		}

		r.SetBranch(branch)
	}

	results, err := r.GitRepository.Test(ctx)
	if err != nil || !results.Success {
		return results, err
	}

	if result := r.checkBranchProtection(ctx); result != nil {
		return result, nil
	}

	return results, nil
}

// checkBranchProtection validates that branch protection rules and repository rulesets
// do not block direct pushes when the write workflow is configured.
// Returns nil if the check passes or is not applicable.
func (r *githubRepository) checkBranchProtection(ctx context.Context) *provisioning.TestResults {
	if !r.hasWriteWorkflow() {
		return nil
	}

	var allReasons []string

	// Check classic branch protection rules
	bp, err := r.gh.GetBranchProtection(ctx, r.owner, r.repo, r.GetCurrentBranch())
	if err != nil {
		// Failed to check branch protection - return error to user
		return &provisioning.TestResults{
			Code:    http.StatusBadRequest,
			Success: false,
			Errors: []provisioning.ErrorDetails{{
				Type:   metav1.CauseTypeFieldValueInvalid,
				Field:  field.NewPath("spec", "github", "branch").String(),
				Detail: fmt.Sprintf("failed to check branch protection for branch %q: %v", r.GetCurrentBranch(), err),
			}},
		}
	}

	if bp != nil {
		if reasons := bp.BlocksDirectPush(); len(reasons) > 0 {
			allReasons = append(allReasons, reasons...)
		}
	}

	// Check repository rulesets
	rulesets, err := r.gh.GetRulesets(ctx, r.owner, r.repo, r.GetCurrentBranch())
	if err != nil {
		// Failed to check rulesets - return error to user
		return &provisioning.TestResults{
			Code:    http.StatusBadRequest,
			Success: false,
			Errors: []provisioning.ErrorDetails{{
				Type:   metav1.CauseTypeFieldValueInvalid,
				Field:  field.NewPath("spec", "github", "branch").String(),
				Detail: fmt.Sprintf("failed to check repository rulesets for branch %q: %v", r.GetCurrentBranch(), err),
			}},
		}
	}

	if rulesets != nil {
		if reasons := rulesets.BlocksDirectPush(); len(reasons) > 0 {
			allReasons = append(allReasons, reasons...)
		}
	}

	// If any blocking rules were found, return error
	if len(allReasons) > 0 {
		return &provisioning.TestResults{
			Code:    http.StatusBadRequest,
			Success: false,
			Errors: []provisioning.ErrorDetails{{
				Type:   metav1.CauseTypeFieldValueInvalid,
				Field:  field.NewPath("spec", "workflows").String(),
				Detail: fmt.Sprintf("branch %q has protection rules that prevent direct pushes: %s; the \"write\" workflow is not compatible with this branch", r.GetCurrentBranch(), strings.Join(allReasons, ", ")),
			}},
		}
	}

	return nil
}

func (r *githubRepository) hasWriteWorkflow() bool {
	for _, w := range r.config.Spec.Workflows {
		if w == provisioning.WriteWorkflow {
			return true
		}
	}
	return false
}

func (r *githubRepository) History(ctx context.Context, path, ref string) ([]provisioning.HistoryItem, error) {
	if ref == "" {
		ref = r.config.Spec.GitHub.Branch
	}

	finalPath := safepath.Join(r.config.Spec.GitHub.Path, path)
	commits, err := r.gh.Commits(ctx, r.owner, r.repo, finalPath, ref)
	if err != nil {
		if errors.Is(err, repository.ErrFileNotFound) {
			return nil, repository.ErrFileNotFound
		}

		return nil, fmt.Errorf("get commits: %w", err)
	}

	ret := make([]provisioning.HistoryItem, 0, len(commits))
	for _, commit := range commits {
		authors := make([]provisioning.Author, 0)
		if commit.Author != nil {
			authors = append(authors, provisioning.Author{
				Name:      commit.Author.Name,
				Username:  commit.Author.Username,
				AvatarURL: commit.Author.AvatarURL,
			})
		}

		if commit.Committer != nil && commit.Author != nil && commit.Author.Name != commit.Committer.Name {
			authors = append(authors, provisioning.Author{
				Name:      commit.Committer.Name,
				Username:  commit.Committer.Username,
				AvatarURL: commit.Committer.AvatarURL,
			})
		}

		ret = append(ret, provisioning.HistoryItem{
			Ref:       commit.Ref,
			Message:   commit.Message,
			Authors:   authors,
			CreatedAt: commit.CreatedAt.UnixMilli(),
		})
	}

	return ret, nil
}

// ListRefs list refs from the git repository and add the ref URL to the ref item
func (r *githubRepository) ListRefs(ctx context.Context) ([]provisioning.RefItem, error) {
	refs, err := r.GitRepository.ListRefs(ctx)
	if err != nil {
		return nil, fmt.Errorf("list refs: %w", err)
	}

	for i := range refs {
		refs[i].RefURL = fmt.Sprintf("%s/tree/%s", r.config.Spec.GitHub.URL, refs[i].Name)
	}

	return refs, nil
}

// ResourceURLs implements RepositoryWithURLs.
func (r *githubRepository) ResourceURLs(ctx context.Context, file *repository.FileInfo) (*provisioning.RepositoryURLs, error) {
	cfg := r.config.Spec.GitHub
	if file.Path == "" || cfg == nil {
		return nil, nil
	}

	ref := file.Ref
	if ref == "" {
		ref = cfg.Branch
	}

	urls := &provisioning.RepositoryURLs{
		RepositoryURL: cfg.URL,
		SourceURL:     fmt.Sprintf("%s/blob/%s/%s", cfg.URL, ref, file.Path),
	}

	if ref != cfg.Branch {
		urls.CompareURL = fmt.Sprintf("%s/compare/%s...%s", cfg.URL, cfg.Branch, ref)

		// Create a new pull request
		urls.NewPullRequestURL = fmt.Sprintf("%s?quick_pull=1&labels=grafana", urls.CompareURL)
	}

	return urls, nil
}

// RefURLs implements RepositoryWithURLs.
func (r *githubRepository) RefURLs(ctx context.Context, ref string) (*provisioning.RepositoryURLs, error) {
	cfg := r.config.Spec.GitHub
	if cfg == nil || ref == "" {
		return nil, nil
	}

	urls := &provisioning.RepositoryURLs{
		SourceURL: fmt.Sprintf("%s/tree/%s", cfg.URL, ref),
	}

	if ref != cfg.Branch {
		urls.CompareURL = fmt.Sprintf("%s/compare/%s...%s", cfg.URL, cfg.Branch, ref)
		urls.NewPullRequestURL = fmt.Sprintf("%s?quick_pull=1&labels=grafana", urls.CompareURL)
	}

	return urls, nil
}
