package rest

import (
	"net/http"
	"strconv"
	"time"

	"github.com/pingcap/agent-git-service/internal/db"
	"github.com/pingcap/agent-git-service/internal/rest/respond"
	"github.com/pingcap/agent-git-service/internal/rest/transform"
	"github.com/pingcap/agent-git-service/internal/service"
)

// ListPullRequestStacks handles GET /api/v3/repos/{owner}/{repo}/stacks.
func (d *Deps) ListPullRequestStacks(w http.ResponseWriter, r *http.Request) {
	page, perPage := parsePagination(r)
	var pullRequestNumber *int
	if raw := r.URL.Query().Get("pull_request"); raw != "" {
		n, err := strconv.Atoi(raw)
		if err != nil || n <= 0 {
			respond.ValidationFailed(w, "pull_request must be a positive integer")
			return
		}
		pullRequestNumber = &n
	}
	stacks, total, err := d.Svc.ListPullRequestStacks(r.Context(), repoFullName(r), service.ListPullRequestStacksOptions{
		PullRequestNumber: pullRequestNumber, Page: page, PerPage: perPage,
	})
	if err != nil {
		respond.ServiceErrorRequest(r, w, err)
		return
	}
	setLinkHeader(w, r, d.Svc.CanonicalBaseURL(r.Context()), int(total), page, perPage)
	out := make([]any, len(stacks))
	for i := range stacks {
		out[i] = d.pullRequestStackListJSON(r, stacks[i])
	}
	respond.JSON(w, http.StatusOK, out)
}

func (d *Deps) pullRequestStackListJSON(r *http.Request, stack service.PullRequestStackView) map[string]any {
	out := d.pullRequestStackJSON(r, stack)
	prs := make([]any, len(stack.Entries))
	for i, entry := range stack.Entries {
		prs[i] = minimalStackPullRequest(entry.PullRequest)
	}
	out["pull_requests"] = prs
	return out
}

// GetPullRequestStack handles GET /api/v3/repos/{owner}/{repo}/stacks/{stack_number}.
func (d *Deps) GetPullRequestStack(w http.ResponseWriter, r *http.Request) {
	number, ok := mustIntParam(w, r, "stack_number")
	if !ok {
		return
	}
	stack, err := d.Svc.GetPullRequestStack(r.Context(), repoFullName(r), number)
	if err != nil {
		respond.ServiceErrorRequest(r, w, err)
		return
	}
	respond.JSON(w, http.StatusOK, d.pullRequestStackJSON(r, stack))
}

// CreatePullRequestStack handles POST /api/v3/repos/{owner}/{repo}/stacks.
func (d *Deps) CreatePullRequestStack(w http.ResponseWriter, r *http.Request) {
	var body struct {
		PullRequests []int `json:"pull_requests"`
	}
	if err := decodeBodyStrict(r, &body); err != nil {
		respond.ValidationFailed(w, "invalid body")
		return
	}
	stack, err := d.Svc.CreatePullRequestStack(r.Context(), repoFullName(r), body.PullRequests)
	if err != nil {
		respond.ServiceErrorRequest(r, w, err)
		return
	}
	d.dispatchStackedPullRequestWebhooks(r, stack, stack.Entries)
	respond.JSON(w, http.StatusCreated, d.pullRequestStackJSON(r, stack))
}

// AddPullRequestsToStack handles POST /api/v3/repos/{owner}/{repo}/stacks/{stack_number}/add.
func (d *Deps) AddPullRequestsToStack(w http.ResponseWriter, r *http.Request) {
	number, ok := mustIntParam(w, r, "stack_number")
	if !ok {
		return
	}
	var body struct {
		PullRequests []int `json:"pull_requests"`
	}
	if err := decodeBodyStrict(r, &body); err != nil {
		respond.ValidationFailed(w, "invalid body")
		return
	}
	before, err := d.Svc.GetPullRequestStack(r.Context(), repoFullName(r), number)
	if err != nil {
		respond.ServiceErrorRequest(r, w, err)
		return
	}
	stack, err := d.Svc.AddPullRequestsToStack(r.Context(), repoFullName(r), number, body.PullRequests)
	if err != nil {
		respond.ServiceErrorRequest(r, w, err)
		return
	}
	if len(stack.Entries) > len(before.Entries) {
		d.dispatchStackedPullRequestWebhooks(r, stack, stack.Entries[len(before.Entries):])
	}
	respond.JSON(w, http.StatusOK, d.pullRequestStackJSON(r, stack))
}

// UnstackPullRequestStack handles POST /api/v3/repos/{owner}/{repo}/stacks/{stack_number}/unstack.
func (d *Deps) UnstackPullRequestStack(w http.ResponseWriter, r *http.Request) {
	number, ok := mustIntParam(w, r, "stack_number")
	if !ok {
		return
	}
	stack, err := d.Svc.UnstackPullRequestStack(r.Context(), repoFullName(r), number)
	if err != nil {
		respond.ServiceErrorRequest(r, w, err)
		return
	}
	if stack == nil {
		w.WriteHeader(http.StatusNoContent)
		return
	}
	respond.JSON(w, http.StatusOK, d.pullRequestStackJSON(r, *stack))
}

func (d *Deps) pullRequestStackJSON(r *http.Request, stack service.PullRequestStackView) map[string]any {
	repoFullName := stack.Repository.FullName
	if repoFullName == "" && len(stack.Entries) > 0 {
		repoFullName = stack.Entries[0].PullRequest.Repository.FullName
	}
	open := false
	tr := transform.FromContext(r.Context())
	prs := make([]any, len(stack.Entries))
	for i, entry := range stack.Entries {
		pr := entry.PullRequest
		if pr.State == db.StateOpen && !pr.Merged {
			open = true
		}
		prs[i] = simpleStackPullRequest(tr, pr)
	}
	return map[string]any{
		"id":            stack.ID,
		"number":        stack.Number,
		"node_id":       transform.NodeID("PullRequestStack", stack.ID),
		"url":           tr.APIBase() + "/repos/" + repoFullName + "/stacks/" + strconv.Itoa(stack.Number),
		"base":          map[string]any{"ref": stack.BaseRef},
		"open":          open,
		"created_at":    stack.CreatedAt,
		"pull_requests": prs,
	}
}

func simpleStackPullRequest(tr transform.Transformer, pr db.PullRequest) map[string]any {
	var mergedAt any
	if pr.MergedAt != nil {
		mergedAt = pr.MergedAt.Format(time.RFC3339)
	}
	headRepo := pr.HeadRepository
	if headRepo.ID == 0 {
		headRepo = pr.Repository
	}
	simpleRepo := func(repo db.Repository) map[string]any {
		return map[string]any{
			"id": repo.ID, "url": tr.APIBase() + "/repos/" + repo.FullName, "name": repo.Name,
		}
	}
	return map[string]any{
		"id": pr.ID, "node_id": transform.NodeID("PullRequest", pr.ID), "title": pr.Title,
		"state": pr.State, "merged_at": mergedAt, "draft": pr.Draft,
		"html_url": tr.HTMLBase() + "/" + pr.Repository.FullName + "/pull/" + strconv.Itoa(pr.Number), "user": stackSimpleUser(tr, pr.Author),
		"number": pr.Number, "url": tr.APIBase() + "/repos/" + pr.Repository.FullName + "/pulls/" + strconv.Itoa(pr.Number),
		"head": map[string]any{"ref": pr.HeadRef, "sha": pr.HeadSHA, "repo": simpleRepo(headRepo)},
		"base": map[string]any{"ref": pr.BaseRef, "sha": pr.BaseSHA, "repo": simpleRepo(pr.Repository)},
	}
}

func stackSimpleUser(tr transform.Transformer, user db.User) map[string]any {
	full := tr.User(user)
	apiURL, _ := full["url"].(string)
	return map[string]any{
		"login": user.Login, "id": user.ID, "node_id": full["node_id"],
		"avatar_url": full["avatar_url"], "gravatar_id": "", "url": apiURL,
		"html_url": full["html_url"], "followers_url": full["followers_url"],
		"following_url": full["following_url"], "gists_url": full["gists_url"],
		"starred_url": full["starred_url"], "subscriptions_url": apiURL + "/subscriptions",
		"organizations_url": full["organizations_url"], "repos_url": full["repos_url"],
		"events_url": full["events_url"], "received_events_url": full["received_events_url"],
		"type": user.Type, "site_admin": user.SiteAdmin, "user_view_type": "public",
	}
}

func minimalStackPullRequest(pr db.PullRequest) map[string]any {
	var mergedAt any
	if pr.MergedAt != nil {
		mergedAt = pr.MergedAt.Format(time.RFC3339)
	}
	return map[string]any{
		"number": pr.Number, "state": pr.State, "draft": pr.Draft, "merged_at": mergedAt,
		"head": map[string]any{"ref": pr.HeadRef, "sha": pr.HeadSHA},
	}
}

func (d *Deps) dispatchStackedPullRequestWebhooks(r *http.Request, stack service.PullRequestStackView, entries []db.PullRequestStackEntry) {
	pullRequestIDs := make([]uint, len(entries))
	for i := range entries {
		pullRequestIDs[i] = entries[i].PullRequestID
	}
	memberships, err := d.Svc.ListPullRequestStackMemberships(r.Context(), pullRequestIDs)
	if err != nil {
		logErr(r.Context(), "StackPR: load webhook memberships", err)
		return
	}
	for _, entry := range entries {
		membership, ok := memberships[entry.PullRequestID]
		if !ok {
			continue
		}
		membershipJSON := stackMembershipJSON(membership)
		prJSON := d.prWithExtras(r, entry.PullRequest)
		prJSON["stack"] = membershipJSON
		payload := d.webhookPRPayload(r.Context(), entry.PullRequest, "stacked", prJSON)
		payload["stack"] = membershipJSON
		logErr(r.Context(), "StackPR: webhook", d.Svc.DispatchWebhookEvent(r.Context(), entry.PullRequest.RepositoryID, "pull_request", "stacked", payload))
	}
}

func stackMembershipJSON(m service.PullRequestStackMembership) map[string]any {
	return map[string]any{
		"id": m.ID, "number": m.Number, "size": m.Size, "position": m.Position,
		"base": map[string]any{"ref": m.BaseRef, "sha": m.BaseSHA},
	}
}
