package common

/*
 * This file is part of Autogits.
 *
 * Copyright © 2024 SUSE LLC
 *
 * Autogits is free software: you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation, either version 2 of the License, or (at your option) any later
 * version.
 *
 * Autogits is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE. See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * Foobar. If not, see <https://www.gnu.org/licenses/>.
 */

import (
	"fmt"
	"io"
	"os"
	"path"
	"path/filepath"
	"slices"
	"time"

	transport "github.com/go-openapi/runtime/client"
	"github.com/go-openapi/strfmt"
	apiclient "src.opensuse.org/autogits/common/gitea-generated/client"
	"src.opensuse.org/autogits/common/gitea-generated/client/notification"
	"src.opensuse.org/autogits/common/gitea-generated/client/organization"
	"src.opensuse.org/autogits/common/gitea-generated/client/repository"
	"src.opensuse.org/autogits/common/gitea-generated/models"
)

//go:generate mockgen -source=gitea_utils.go -destination=mock/gitea_utils.go -typed

// maintainer list file in ProjectGit
const (
	MaintainershipFile = "_maitnainership.json"
	MaintainershipDir  = "maintaineirship"
)

const (
	// from Gitea
	// ReviewStateApproved pr is approved
	ReviewStateApproved models.ReviewStateType = "APPROVED"
	// ReviewStatePending pr state is pending
	ReviewStatePending models.ReviewStateType = "PENDING"
	// ReviewStateComment is a comment review
	ReviewStateComment models.ReviewStateType = "COMMENT"
	// ReviewStateRequestChanges changes for pr are requested
	ReviewStateRequestChanges models.ReviewStateType = "REQUEST_CHANGES"
	// ReviewStateRequestReview review is requested from user
	ReviewStateRequestReview models.ReviewStateType = "REQUEST_REVIEW"
	// ReviewStateUnknown state of pr is unknown
	ReviewStateUnknown models.ReviewStateType = ""
)

type GiteaMaintainershipInterface interface {
	FetchMaintainershipFile(org, prjGit, branch string) ([]byte, error)
	FetchMaintainershipDirFile(org, prjGit, branch, pkg string) ([]byte, error)
}

type GiteaPRFetcher interface {
	GetPullRequest(org, project string, num int64) (*models.PullRequest, error)
}

type GiteaReviewFetcher interface {
	GetPullRequestReviews(org, project string, PRnum int64) ([]*models.PullReview, error)
}

type GiteaReviewRequester interface {
	RequestReviews(pr *models.PullRequest, reviewer string) ([]*models.PullReview, error)
}

type GiteaReviewer interface {
	AddReviewComment(pr *models.PullRequest, state models.ReviewStateType, comment string) (*models.PullReview, error)
}

type Gitea interface {
	GiteaReviewRequester
	GiteaReviewer
	GiteaPRFetcher

	GetPullNotifications(since *time.Time) ([]*models.NotificationThread, error)
	SetNotificationRead(notificationId int64) error
	GetOrganization(orgName string) (*models.Organization, error)
	GetOrganizationRepositories(orgName string) ([]*models.Repository, error)
	CreateRepositoryIfNotExist(git Git, org Organization, repoName string) (*models.Repository, error)
	CreatePullRequestIfNotExist(repo *models.Repository, srcId, targetId, title, body string) (*models.PullRequest, error)
	GetRepositoryFileContent(org, repo, hash, path string) ([]byte, error)
	GetPullRequestFileContent(pr *models.PullRequest, path string) ([]byte, error)
	GetRecentPullRequests(org, repo string) ([]*models.PullRequest, error)
	GetRecentCommits(org, repo, branch string, commitNo int64) ([]*models.Commit, error)

	GiteaMaintainershipInterface
}

type GiteaTransport struct {
	transport *transport.Runtime
	client    *apiclient.GiteaAPI
}

func AllocateGiteaTransport(host string) Gitea {
	var r GiteaTransport

	r.transport = transport.New(host, apiclient.DefaultBasePath, [](string){"https"})
	r.transport.DefaultAuthentication = transport.BearerToken(giteaToken)

	r.client = apiclient.New(r.transport, nil)

	return &r
}

func (gitea *GiteaTransport) FetchMaintainershipFile(org, repo, branch string) ([]byte, error) {
	return gitea.GetRepositoryFileContent(org, repo, branch, MaintainershipFile)
}

func (gitea *GiteaTransport) FetchMaintainershipDirFile(org, repo, branch, pkg string) ([]byte, error) {
	return gitea.GetRepositoryFileContent(org, repo, branch, path.Join(MaintainershipDir, pkg))
}

func (gitea *GiteaTransport) GetPullRequest(org, project string, num int64) (*models.PullRequest, error) {
	pr, err := gitea.client.Repository.RepoGetPullRequest(
		repository.NewRepoGetPullRequestParams().
			WithDefaults().
			WithOwner(org).
			WithRepo(project).
			WithIndex(num),
		gitea.transport.DefaultAuthentication,
	)

	return pr.Payload, err
}

func (gitea *GiteaTransport) GetPullReviews(org, project string, num int64) ([]*models.PullReview, error) {
	limit := int64(20)
	var page int64

	var allReviews []*models.PullReview

	for {
		reviews, err := gitea.client.Repository.RepoListPullReviews(
			repository.NewRepoListPullReviewsParams().
				WithDefaults().
				WithOwner(org).
				WithRepo(project).
				WithIndex(num).
				WithPage(&page).
				WithLimit(&limit),
			gitea.transport.DefaultAuthentication,
		)

		if err != nil {
			return nil, err
		}

		allReviews = slices.Concat(allReviews, reviews.Payload)
		if len(reviews.Payload) < int(limit) {
			break
		}
		page++
	}

	return allReviews, nil
}

func (gitea *GiteaTransport) GetPullNotifications(since *time.Time) ([]*models.NotificationThread, error) {
	bigLimit := int64(100000)

	params := notification.NewNotifyGetListParams().
		WithDefaults().
		WithSubjectType([]string{"Pull"}).
		WithStatusTypes([]string{"unread"}).
		WithLimit(&bigLimit)

	if since != nil {
		s := strfmt.DateTime(*since)
		params.SetSince(&s)
	}

	list, err := gitea.client.Notification.NotifyGetList(params, gitea.transport.DefaultAuthentication)
	if err != nil {
		return nil, err
	}

	return list.Payload, nil
}

func (gitea *GiteaTransport) SetNotificationRead(notificationId int64) error {
	_, err := gitea.client.Notification.NotifyReadThread(
		notification.NewNotifyReadThreadParams().
			WithDefaults().
			WithID(fmt.Sprint(notificationId)),
		gitea.transport.DefaultAuthentication,
	)

	if err != nil {
		return fmt.Errorf("Error setting notification: %d. Err: %w", notificationId, err)
	}

	return nil
}

func (gitea *GiteaTransport) GetOrganization(orgName string) (*models.Organization, error) {
	org, err := gitea.client.Organization.OrgGet(
		organization.NewOrgGetParams().WithOrg(orgName),
		gitea.transport.DefaultAuthentication,
	)
	if err != nil {
		return nil, fmt.Errorf("Error fetching org: '%s' data. Err: %w", orgName, err)
	}
	return org.Payload, nil
}

func (gitea *GiteaTransport) GetOrganizationRepositories(orgName string) ([]*models.Repository, error) {
	var page int64
	repos := make([]*models.Repository, 0, 100)

	page = 1
	for {
		ret, err := gitea.client.Organization.OrgListRepos(
			organization.NewOrgListReposParams().WithOrg(orgName).WithPage(&page),
			gitea.transport.DefaultAuthentication,
		)

		if err != nil {
			return nil, fmt.Errorf("Error retrieving repository list for org: '%s'. Err: %w", orgName, err)
		}

		if len(ret.Payload) == 0 {
			break
		}

		repos = append(repos, ret.Payload...)
		page++
	}

	return repos, nil
}

func (gitea *GiteaTransport) CreateRepositoryIfNotExist(git Git, org Organization, repoName string) (*models.Repository, error) {
	repo, err := gitea.client.Repository.RepoGet(
		repository.NewRepoGetParams().WithDefaults().WithOwner(org.Username).WithRepo(repoName),
		gitea.transport.DefaultAuthentication)

	if err != nil {
		switch err.(type) {
		case *repository.RepoGetNotFound:
			repo, err := gitea.client.Organization.CreateOrgRepo(
				organization.NewCreateOrgRepoParams().WithDefaults().WithBody(
					&models.CreateRepoOption{
						AutoInit:         false,
						Name:             &repoName,
						ObjectFormatName: models.CreateRepoOptionObjectFormatNameSha256,
					},
				).WithOrg(org.Username),
				nil,
			)

			if err != nil {
				switch err.(type) {
				case *organization.CreateOrgRepoCreated:
				// weird, but ok, repo created
				default:
					return nil, fmt.Errorf("error creating repo '%s' under '%s'. Err: %w", repoName, org.Username, err)
				}
			}

			// initialize repository
			if err = os.Mkdir(filepath.Join(git.GetPath(), DefaultGitPrj), 0700); err != nil {
				return nil, err
			}
			if err = git.GitExec(DefaultGitPrj, "init", "--object-format="+repo.Payload.ObjectFormatName); err != nil {
				return nil, err
			}
			if err = git.GitExec(DefaultGitPrj, "checkout", "-b", repo.Payload.DefaultBranch); err != nil {
				return nil, err
			}
			readmeFilename := filepath.Join(git.GetPath(), DefaultGitPrj, "README.md")
			{
				file, _ := os.Create(readmeFilename)
				defer file.Close()

				io.WriteString(file, ReadmeBoilerplate)
			}
			if err = git.GitExec(DefaultGitPrj, "add", "README.md"); err != nil {
				return nil, err
			}
			if err = git.GitExec(DefaultGitPrj, "commit", "-m", "Automatic devel project creation"); err != nil {
				return nil, err
			}
			if err = git.GitExec(DefaultGitPrj, "remote", "add", "origin", repo.Payload.SSHURL); err != nil {
				return nil, err
			}

			return repo.Payload, nil
		default:
			return nil, fmt.Errorf("cannot fetch repo data for '%s' / '%s' : %w", org.Username, repoName, err)
		}
	}

	return repo.Payload, nil
}

func (gitea *GiteaTransport) CreatePullRequestIfNotExist(repo *models.Repository, srcId, targetId, title, body string) (*models.PullRequest, error) {
	prOptions := models.CreatePullRequestOption{
		Base:  repo.DefaultBranch,
		Head:  srcId,
		Title: title,
		Body:  body,
	}

	if pr, err := gitea.client.Repository.RepoGetPullRequestByBaseHead(
		repository.NewRepoGetPullRequestByBaseHeadParams().WithOwner(repo.Owner.UserName).WithRepo(repo.Name).WithBase(repo.DefaultBranch).WithHead(srcId),
		gitea.transport.DefaultAuthentication,
	); err == nil {
		return pr.Payload, nil
	}

	pr, err := gitea.client.Repository.RepoCreatePullRequest(
		repository.
			NewRepoCreatePullRequestParams().
			WithDefaults().
			WithOwner(repo.Owner.UserName).
			WithRepo(repo.Name).
			WithBody(&prOptions),
		gitea.transport.DefaultAuthentication,
	)

	if err != nil {
		return nil, fmt.Errorf("Cannot create pull request. %w", err)
	}

	return pr.GetPayload(), nil
}

func (gitea *GiteaTransport) RequestReviews(pr *models.PullRequest, reviewer string) ([]*models.PullReview, error) {
	reviewOptions := models.PullReviewRequestOptions{
		Reviewers: []string{reviewer},
	}

	review, err := gitea.client.Repository.RepoCreatePullReviewRequests(
		repository.
			NewRepoCreatePullReviewRequestsParams().
			WithOwner(pr.Base.Repo.Owner.UserName).
			WithRepo(pr.Base.Repo.Name).
			WithIndex(pr.Index).
			WithBody(&reviewOptions),
		gitea.transport.DefaultAuthentication,
	)

	if err != nil {
		return nil, fmt.Errorf("Cannot create pull request: %w", err)
	}

	return review.GetPayload(), nil
}

func (gitea *GiteaTransport) AddReviewComment(pr *models.PullRequest, state models.ReviewStateType, comment string) (*models.PullReview, error) {
	c, err := gitea.client.Repository.RepoCreatePullReview(
		repository.NewRepoCreatePullReviewParams().
			WithDefaults().
			WithOwner(pr.Base.Repo.Owner.UserName).
			WithRepo(pr.Base.Repo.Name).
			WithIndex(pr.Index).
			WithBody(&models.CreatePullReviewOptions{
				Event: state,
				Body:  comment,
			}),
		gitea.transport.DefaultAuthentication,
	)
	/*
		c, err := client.Repository.RepoSubmitPullReview(
			repository.NewRepoSubmitPullReviewParams().
				WithDefaults().
				WithOwner(pr.Base.Repo.Owner.UserName).
				WithRepo(pr.Base.Repo.Name).
				WithIndex(pr.Index).
				WithID(review.ID).
				WithBody(&models.SubmitPullReviewOptions{
					Event: state,
					Body:  comment,
				}),
			transport.DefaultAuthentication,
		)
	*/

	/*	c, err := client.Issue.IssueCreateComment(
		issue.NewIssueCreateCommentParams().
			WithDefaults().
			WithOwner(pr.Base.Repo.Owner.UserName).
			WithRepo(pr.Base.Repo.Name).
			WithIndex(pr.Index).
			WithBody(&models.CreateIssueCommentOption{
				Body: &comment,
			}),
		transport.DefaultAuthentication)
	*/
	if err != nil {
		return nil, err
	}

	return c.Payload, nil
}

func (gitea *GiteaTransport) GetRepositoryFileContent(org, repo, hash, path string) ([]byte, error) {
	var retData []byte

	dataOut := writeFunc(func(data []byte) (int, error) {
		if len(data) == 0 {
			return 0, nil
		}
		retData = data
		return len(data), nil
	})
	_, err := gitea.client.Repository.RepoGetRawFile(
		repository.NewRepoGetRawFileParams().
			WithOwner(org).
			WithRepo(repo).
			WithFilepath(path).
			WithRef(&hash),
		gitea.transport.DefaultAuthentication,
		dataOut,
		repository.WithContentTypeApplicationOctetStream,
	)

	if err != nil {
		return nil, err
	}

	return retData, nil
}

func (gitea *GiteaTransport) GetPullRequestFileContent(pr *models.PullRequest, path string) ([]byte, error) {
	return gitea.GetRepositoryFileContent(pr.Head.Repo.Owner.UserName, pr.Head.Repo.Name, pr.Head.Sha, path)
}

func (gitea *GiteaTransport) GetRecentPullRequests(org, repo string) ([]*models.PullRequest, error) {
	prs := make([]*models.PullRequest, 0, 10)
	var page int64
	page = 1
	sort := "recentupdate"

	for {
		res, err := gitea.client.Repository.RepoListPullRequests(
			repository.NewRepoListPullRequestsParams().
				WithOwner(org).
				WithRepo(repo).
				WithPage(&page).
				WithSort(&sort),
			gitea.transport.DefaultAuthentication)
		if err != nil {
			return nil, err
		}

		prs = append(prs, res.Payload...)
		n := len(res.Payload)
		if n < 10 {
			break
		}

		// if pr is closed for more than a week, assume that we are done too
		if time.Since(time.Time(res.Payload[n-1].Updated)) > 7*24*time.Hour {
			break
		}

		page++
	}

	return prs, nil
}

func (gitea *GiteaTransport) GetRecentCommits(org, repo, branch string, commitNo int64) ([]*models.Commit, error) {
	not := false
	var page int64
	page = 1
	commits, err := gitea.client.Repository.RepoGetAllCommits(
		repository.NewRepoGetAllCommitsParams().
			WithOwner(org).
			WithRepo(repo).
			WithSha(&branch).
			WithPage(&page).
			WithStat(&not).
			WithFiles(&not).
			WithVerification(&not).
			WithLimit(&commitNo),
		gitea.transport.DefaultAuthentication,
	)

	if err != nil {
		return nil, err
	}

	return commits.Payload, nil
}