package common

import (
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"slices"
	"strings"
	"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"
)

const PrPattern = "PR: %s/%s#%d"

func (h *RequestHandler) allocateGiteaTransport() (*transport.Runtime, *apiclient.GiteaAPI) {
	r := transport.New("src.opensuse.org", apiclient.DefaultBasePath, [](string){"https"})
	r.DefaultAuthentication = transport.BearerToken(giteaToken)
	//	r.SetDebug(true)

	return r, apiclient.New(r, nil)
}

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 = ""
)

func (h *RequestHandler) GetPullRequestAndReviews(org, project string, num int64) (*models.PullRequest, []*models.PullReview, error) {
	if h.HasError() {
		return nil, nil, h.Error
	}

	transport, client := h.allocateGiteaTransport()
	pr, err := client.Repository.RepoGetPullRequest(
		repository.NewRepoGetPullRequestParams().
			WithDefaults().
			WithOwner(org).
			WithRepo(project).
			WithIndex(num),
		transport.DefaultAuthentication,
	)

	if err != nil {
		h.Error = err
		h.ErrLogger.Println(err.Error())
		return nil, nil, err
	}

	limit := int64(1000)
	reviews, err := client.Repository.RepoListPullReviews(
		repository.NewRepoListPullReviewsParams().
			WithDefaults().
			WithOwner(org).
			WithRepo(project).
			WithIndex(num).
			WithLimit(&limit),
		transport.DefaultAuthentication,
	)

	if err != nil {
		h.Error = err
		h.ErrLogger.Println(err.Error())
		return nil, nil, err
	}

	return pr.Payload, reviews.Payload, nil
}

func (h *RequestHandler) GetPullNotifications(since *time.Time) ([]*models.NotificationThread, error) {
	if h.HasError() {
		return nil, h.Error
	}

	bigLimit := int64(100000)
	transport, client := h.allocateGiteaTransport()

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

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

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

	if !list.IsSuccess() {
		h.Error = fmt.Errorf("Cannot fetch notifications: %s", list.Error())
		return nil, h.Error
	}

	return list.Payload, nil
}

func (h *RequestHandler) SetNotificationRead(notificationId int64) error {
	if h.HasError() {
		return h.Error
	}

	transport, client := h.allocateGiteaTransport()
	list, err := client.Notification.NotifyReadThread(
		notification.NewNotifyReadThreadParams().
			WithDefaults().
			WithID(fmt.Sprint(notificationId)),
		transport.DefaultAuthentication,
	)

	if err != nil {
		h.ErrLogger.Printf("Error setting notification: %d: %v\n", notificationId, err)
		h.Error = err
		return err
	}

	if !list.IsSuccess() {
		h.Error = fmt.Errorf("Cannot update notifications: %d", notificationId)
		return h.Error
	}

	return nil
}

func (h *RequestHandler) CreateRepositoryIfNotExist(org Organization, repoName string) *models.Repository {
	if h.HasError() {
		return nil
	}

	transport, client := h.allocateGiteaTransport()
	repo, err := client.Repository.RepoGet(
		repository.NewRepoGetParams().WithDefaults().WithOwner(org.Username).WithRepo(repoName),
		transport.DefaultAuthentication)

	if err != nil {
		switch err.(type) {
		case *repository.RepoGetNotFound:
			h.StdLogger.Printf("repo '%s' does not exist. Trying to create it ....\n", repoName)

			repo, err := 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:
					h.StdLogger.Printf("repo '%s' created, with notification error?\n", repoName)
				default:
					h.Error = err
					h.ErrLogger.Printf("error creating repo '%s' under '%s': %s\n", repoName, org.Username, err.Error())
					return nil
				}
			} else {
				h.StdLogger.Printf("repo '%s' created\n", repoName)
			}

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

				io.WriteString(file, ReadmeBoilerplate)
			}
			h.GitExec(DefaultGitPrj, "add", "README.md")
			h.GitExec(DefaultGitPrj, "commit", "-m", "Automatic devel project creation")
			h.GitExec(DefaultGitPrj, "remote", "add", "origin", repo.Payload.SSHURL)

			return repo.Payload
		default:
			h.Error = err
			h.ErrLogger.Printf("cannot fetch repo data for '%s' / '%s' : %v\n", org.Username, repoName, err)
		}
	}

	return repo.Payload
}

func (h *RequestHandler) CreatePullRequest(repo *models.Repository, srcId, targetId, title, body string) *models.PullRequest {
	if h.HasError() {
		return nil
	}

	transport, client := h.allocateGiteaTransport()

	prOptions := models.CreatePullRequestOption{
		Base:  repo.DefaultBranch,
		Head:  srcId,
		Title: title,
		Body:  body,
	}

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

	if err != nil {
		h.ErrLogger.Printf("Cannot create pull request: %v\n", err)
		h.Error = err
		return nil
	}

	if !pr.IsSuccess() {
		h.ErrLogger.Printf("PR creation failed: %s\n", pr.Error())
		h.Error = errors.New(pr.Error())
		return nil
	}

	return pr.GetPayload()
}

func (h *RequestHandler) RequestReviews(pr *models.PullRequest, reviewer string) []*models.PullReview {
	if h.HasError() {
		return nil
	}

	transport, client := h.allocateGiteaTransport()

	reviewOptions := models.PullReviewRequestOptions{
		Reviewers: []string{reviewer},
	}

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

	if err != nil {
		h.ErrLogger.Printf("Cannot create pull request: %v\n", err)
		h.Error = err
		return nil
	}

	if !review.IsSuccess() {
		h.ErrLogger.Printf("PR creation failed: %s\n", review.Error())
		h.Error = errors.New(review.Error())
		return nil
	}

	return review.GetPayload()
}

func (h *RequestHandler) IsReviewed(pr *models.PullRequest) (bool, error) {
	if h.HasError() {
		return false, h.Error
	}

	transport, client := h.allocateGiteaTransport()
	// TODO: get review from project git
	reviewers := pr.RequestedReviewers
	var page, limit int64
	var reviews []*models.PullReview
	page = 0
	limit = 20
	for {
		res, err := client.Repository.RepoListPullReviews(
			repository.NewRepoListPullReviewsParams().
				WithOwner(pr.Base.Repo.Owner.UserName).
				WithRepo(pr.Base.Repo.Name).
				WithPage(&page).
				WithLimit(&limit),
			transport.DefaultAuthentication)

		if err != nil {
			return false, err
		}

		if res.IsSuccess() {
			r := res.Payload

			if reviews == nil {
				reviews = r
			} else {
				reviews = append(reviews, r...)
			}

			if len(r) < int(limit) {
				break
			}
		}
	}

	slices.Reverse(reviews)

	for _, review := range reviews {
		if review.Stale || review.Dismissed {
			continue
		}

		next_review:
		for i, reviewer := range reviewers {
			if review.User.UserName == reviewer.UserName {
				switch (review.State) {
				case ReviewStateApproved:
					reviewers = slices.Delete(reviewers, i, i)
					break next_review
				case ReviewStateRequestChanges:
					return false, nil
				}
			}
		}
	}

	return len(reviewers) == 0, nil
}

func (h *RequestHandler) AddReviewComment(pr *models.PullRequest, state models.ReviewStateType, comment string) (*models.PullReview, error) {
	transport, client := h.allocateGiteaTransport()

	h.StdLogger.Printf("%#v", *pr)

	c, err := 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,
			}),
		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 (h *RequestHandler) GetAssociatedPrjGitPR(pr *PullRequestAction) *models.PullRequest {
	if h.HasError() {
		return nil
	}

	transport, client := h.allocateGiteaTransport()

	var page, maxSize int64
	page = 1
	maxSize = 10000
	state := "open"
	prs, err := client.Repository.RepoListPullRequests(
		repository.
			NewRepoListPullRequestsParams().
			WithDefaults().
			WithOwner(pr.Repository.Owner.Username).
			WithRepo(DefaultGitPrj).
			WithState(&state).
			WithLimit(&maxSize).
			WithPage(&page),
		transport.DefaultAuthentication)

	if err != nil {
		h.Error = fmt.Errorf("cannot fetch PR list for %s / %s : %v", pr.Repository.Owner.Username, pr.Repository.Name, err)
		return nil
	}

	if !prs.IsSuccess() {
		h.Error = fmt.Errorf("cannot fetch PR list for %s / %s : %s", pr.Repository.Owner.Username, pr.Repository.Name, prs.Error())
	}

	prLine := fmt.Sprintf(PrPattern, pr.Repository.Owner.Username, pr.Repository.Name, pr.Number)
	h.StdLogger.Printf("attemping to match line: '%s'\n", prLine)

	//	payload_processing:
	for _, pr := range prs.Payload {
		lines := strings.Split(pr.Body, "\n")

		for _, line := range lines {
			if strings.TrimSpace(line) == prLine {
				return pr
			}
		}
	}

	return nil
}

func (h *RequestHandler) GetRepositoryFileContent(repo *models.Repository, hash, path string) ([]byte, error) {
	if h.HasError() {
		return nil, h.Error
	}

	transport, client := h.allocateGiteaTransport()
	var retData []byte

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

	if err != nil {
		return nil, err
	}

	if !file.IsSuccess() {
		return nil, fmt.Errorf("Invalid response from server (%d): %s", file.Code(), file.Error())
	}

	return retData, nil
}

func (h *RequestHandler) GetPullRequestFileContent(pr *models.PullRequest, path string) ([]byte, error) {
	return h.GetRepositoryFileContent(pr.Head.Repo, pr.Head.Sha, path)
}
