package webhook

import (
	"context"
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"sync"
	"time"

	userv1 "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/api/core-server/user/v1"
	coreuser "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/entity/core-server/user/v1"
	"github.com/tidbcloud/devops/api"
	webhookapi "github.com/tidbcloud/devops/api/webhook"
	apprent "github.com/tidbcloud/devops/entity/approval"
	ctrl "github.com/tidbcloud/devops/pkg/controller"
	"github.com/tidbcloud/devops/pkg/infra/clients/aiopscli"
	"github.com/tidbcloud/devops/pkg/infra/clients/jiracli"
	"github.com/tidbcloud/devops/pkg/infra/clients/larkcli"
	larkclichat "github.com/tidbcloud/devops/pkg/infra/clients/larkcli/chat"
	larkcliim "github.com/tidbcloud/devops/pkg/infra/clients/larkcli/im"
	larkclinotify "github.com/tidbcloud/devops/pkg/infra/clients/larkcli/notify"
	"github.com/tidbcloud/devops/pkg/infra/config"
	"github.com/tidbcloud/devops/pkg/infra/consts"
	"github.com/tidbcloud/devops/pkg/infra/utils/container"
	apprsvcapi "github.com/tidbcloud/devops/pkg/service/approval/api"
	relsvcapi "github.com/tidbcloud/devops/pkg/service/release/api"

	"github.com/andygrunwald/go-jira"
	"github.com/gin-gonic/gin"
	"github.com/go-logr/logr"
	larkcard "github.com/larksuite/oapi-sdk-go/v3/card"
	larkevent "github.com/larksuite/oapi-sdk-go/v3/event"
	"go.uber.org/dig"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

type ControllerParam struct {
	dig.In

	Conf            *config.RootConfig
	Logger          logr.Logger
	Release         relsvcapi.ServiceAPI
	ApprovalService apprsvcapi.ApprovalServiceAPI
	UserService     userv1.UserServiceClient
	LarkCli         *larkcli.LarkClient
	AIOpsCli        aiopscli.Client
	JiraClient      jiracli.JiraClient
}

// WebhookController provide the api to manage releases.
type WebhookController struct {
	conf       config.WebhookControllerConfig
	larkConfig config.LarkConfig
	logger     logr.Logger
	relSvc     relsvcapi.ReleaseServiceAPI
	apprSvc    apprsvcapi.ApprovalServiceAPI
	userSvc    userv1.UserServiceClient
	larkcli    *larkcli.LarkClient
	aiopscli   aiopscli.Client
	jiracli    jiracli.JiraClient

	LarkEventDispatcher *LarkEventDispatcher
}

func (c *WebhookController) Name() string {
	return "WebhookController"
}

func NewWebhookController(p ControllerParam) ctrl.Controller {
	c := &WebhookController{
		conf:       p.Conf.Controllers.Webhook,
		larkConfig: p.Conf.Clients.Lark,
		logger:     p.Logger.WithName("WebhookController"),
		relSvc:     p.Release,
		apprSvc:    p.ApprovalService,
		userSvc:    p.UserService,
		larkcli:    p.LarkCli,
		aiopscli:   p.AIOpsCli,
		jiracli:    p.JiraClient,
	}

	c.LarkEventDispatcher = &LarkEventDispatcher{
		logger:            c.logger.WithName("LarkEventDispatcher"),
		eventType2Handler: make(map[string][]larkevent.EventHandler),
	}

	return c
}

func (c *WebhookController) RegisterAPI(suite *ctrl.Suite) error {
	r := suite.Router
	rgroup := r.Group("/webhook")
	{
		rgroup.POST("/jira/sync-lark-group", c.JiraBindLarkGroupWhenEscalation)
		rgroup.POST("/jira/invite-assignee", c.JiraInviteAssigneeToLarkGroup)
		rgroup.POST("/jira/create-lark-group-for-s1-tickets", c.JiraCreateLarkGroupForS1Tickets)
	}

	er := suite.ExternalRouter
	egroup := er.Group("/webhook")
	{
		mwBuilder := ctrl.LarkMiddlewareBuiler{
			EncryptKey:          c.larkConfig.Global.EncryptKey,
			VerificationToken:   c.larkConfig.Global.VerificationToken,
			Logger:              c.logger,
			CardEventProxyKey:   c.larkConfig.CardEventProxyKey,
			CardEventProxyAddrs: c.larkConfig.CardEventProxyAddrs,
		}
		egroup.Group("/lark/card").
			Use(mwBuilder.CardMiddleware()).
			POST("/callback", c.HandleLarkCardEvent)
		egroup.Group("/lark/event").
			Use(mwBuilder.EventMiddleware()).
			POST("/callback", c.HandleLarkEvent)
	}

	return nil
}

// JiraBindLarkGroupWhenEscalation
//
//	@Id				WebhookControllerJiraBindLarkGroupWhenEscalation
//	@Description	bind escalated Jira ticket to Lark group
//	@Tags			WebhookController
//	@Accept			json
//	@Produce		json
//	@Param			req		body		webhookapi.JiraBindLarkGroupWhenEscalationRequest	true	"request param"
//	@Success		200		{object}	api.BaseResp
//	@Failure		default	{object}	api.BaseResp
//	@Router			/api/v1/devops/webhook/jira/sync-lark-group [post]
func (c *WebhookController) JiraBindLarkGroupWhenEscalation(ctx *gin.Context) {
	req := &webhookapi.JiraBindLarkGroupWhenEscalationRequest{}
	err := ctx.ShouldBindJSON(req)
	if err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
		return
	}

	c.logger.Info("handle jira bind lark group when escalation", "req", req)

	// get binding lark group from plugin
	groupID, err := c.jiracli.GetBindingLarkGroupOfIssue(ctx, req.IssueID)
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}
	if groupID == "" {
		c.logger.Info("no lark group binded to the issue, skip binding", "issue_id", req.IssueID, "issue_key", req.IssueKey)
		ctrl.Success(ctx, nil)
		return
	}

	// update lark group in jira plugin
	err = c.jiracli.BindLarkGroupToIssue(ctx, req.OutwardIssueID, groupID)
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}

	c.logger.Info("successfully binded lark group when escalation", "issue",
		req.IssueKey, "issue_id", req.IssueID,
		"outward_issue_id", req.OutwardIssueID, "outward_issue_key", req.OutwardIssueKey,
		"binded_lark_group", groupID,
	)
	ctrl.Success(ctx, nil)
}

// JiraInviteAssigneeToLarkGroup
//
//	@Id				WebhookControllerJiraInviteAssigneeToLarkGroup
//	@Description	invite assignee to the lark group if the issue binded to a lark group
//	@Tags			WebhookController
//	@Accept			json
//	@Produce		json
//	@Param			req		body		webhookapi.JiraInviteAssigneeToLarkGroupRequest	true	"request param"
//	@Success		200		{object}	api.BaseResp
//	@Failure		default	{object}	api.BaseResp
//	@Router			/api/v1/devops/webhook/jira/invite-assignee [post]
func (c *WebhookController) JiraInviteAssigneeToLarkGroup(ctx *gin.Context) {
	req := &webhookapi.JiraInviteAssigneeToLarkGroupRequest{}
	err := ctx.ShouldBindJSON(req)
	if err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
		return
	}

	// get assignee from user service
	assigneeResp, err := c.userSvc.GetUser(ctx, &userv1.GetUserRequest{Email: req.AssigneeEmail})
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}

	// get binding lark group from plugin
	groupID, err := c.jiracli.GetBindingLarkGroupOfIssue(ctx, req.IssueID)
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}
	if groupID == "" {
		c.logger.Info("no lark group binded to the issue, skip inviting assignee", "issue_id", req.IssueID, "issue_key", req.IssueKey, "assignee_email", req.AssigneeEmail)
		ctrl.Success(ctx, nil)
		return
	}

	// invite assignee to lark group
	err = c.larkcli.Chat.InviteMembers(ctx, groupID, "user_id", []string{assigneeResp.User.LarkUserId})
	if err != nil {
		ctrl.FailWithCode(ctx, fmt.Errorf("invite assignee %s to lark group %s failed: %w", req.AssigneeEmail, groupID, err), api.ErrCodeInternal)
		return
	}

	ctrl.Success(ctx, nil)
}

// JiraCreateLarkGroupForS1Tickets
//
//	@Id				WebhookControllerJiraCreateLarkGroupForS1Tickets
//	@Description	create a lark group for s1 jira tickets and bind the group to the ticket
//	@Tags			WebhookController
//	@Accept			json
//	@Produce		json
//	@Param			req		body		webhookapi.JiraCreateLarkGroupForS1TicketsRequest	true	"request param"
//	@Success		200		{object}	webhookapi.JiraCreateLarkGroupForS1TicketsResponse
//	@Failure		default	{object}	api.BaseResp
//	@Router			/api/v1/devops/webhook/jira/create-lark-group-for-s1-tickets [post]
func (c *WebhookController) JiraCreateLarkGroupForS1Tickets(ctx *gin.Context) {
	if !c.conf.S1TicketScheduleEnabled {
		ctrl.Success(ctx, &webhookapi.JiraCreateLarkGroupForS1TicketsResponse{Detail: "disabled"})
		return
	}

	req := &webhookapi.JiraCreateLarkGroupForS1TicketsRequest{}
	err := ctx.ShouldBindJSON(req)
	if err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
		return
	}

	now := time.Now()
	emails := []string{}
	for _, sched := range c.conf.S1TicketSchedules {
		if now.Hour() >= sched.StartHour && now.Hour() < sched.EndHour {
			emails = append(emails, sched.Emails...)
		}
	}

	// query issue
	issue, err := c.jiracli.GetIssue(ctx, req.IssueKey, &jira.GetQueryOptions{
		Fields: "summary,assignee,description,versions,components,attachment,customfield_10050",
	})
	if err != nil {
		ctrl.Fail(ctx, fmt.Errorf("get jira issue %s failed: %w", req.IssueKey, err))
		return
	}

	// query users
	if issue.Fields.Assignee != nil && issue.Fields.Assignee.EmailAddress != "" {
		emails = append(emails, issue.Fields.Assignee.EmailAddress)
	}
	usersResp, err := c.userSvc.ListUsers(ctx, &userv1.ListUsersRequest{
		EmailIn: emails,
		Page:    1,
		PerPage: 100,
	})
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}

	// create a lark group
	createChatReq := larkclichat.CreateChatRequest{
		Name:        fmt.Sprintf("%s_%s", req.IssueKey, issue.Fields.Summary),
		Description: "",
		UserIDType:  "user_id",
		MemberIDs:   container.TransformSlice(usersResp.Users, func(u *coreuser.User) string { return u.LarkUserId }),
	}
	if len(createChatReq.MemberIDs) == 0 {
		ctrl.Success(ctx, &webhookapi.JiraCreateLarkGroupForS1TicketsResponse{Detail: "no on-duty user"})
		return
	}
	if len(createChatReq.Name) > 64 {
		createChatReq.Name = createChatReq.Name[:64] + "..."
	}
	groupID, err := c.larkcli.Chat.CreateChat(ctx, createChatReq)
	if err != nil {
		ctrl.FailWithCode(ctx, fmt.Errorf("create lark group for jira issue %s failed: %w", req.IssueKey, err), api.ErrCodeInternal)
		return
	}

	// send jira issue information to the lark group
	comps := container.TransformSlice(issue.Fields.Components, func(compInJira *jira.Component) string {
		return compInJira.Name
	})
	versions := container.TransformSlice(issue.Fields.AffectsVersions, func(verInJira *jira.AffectsVersion) string {
		return verInJira.Name
	})
	env, err := issue.Fields.Unknowns.String("customfield_10050/value")
	if err != nil {
		env = "[load failed]"
	}
	cardMsgID, err := c.larkcli.Notify.NotifyJiraIssueForS1Ticket(ctx, *groupID, larkclinotify.JiraIssueInformationForS1TicketsParam{
		IssueKey:        issue.Key,
		IssueSummary:    issue.Fields.Summary,
		Components:      strings.Join(comps, ","),
		AffectsVersions: strings.Join(versions, ","),
		Environment:     env,
		Description:     issue.Fields.Description,
		DetailLink:      fmt.Sprintf("https://tidb.atlassian.net/browse/%s", req.IssueKey),
	})
	if err != nil {
		ctrl.FailWithCode(ctx, fmt.Errorf("send jira issue information to lark group %s failed: %w", *groupID, err), api.ErrCodeInternal)
		return
	}

	// update lark group in jira plugin
	err = c.jiracli.BindLarkGroupToIssue(ctx, issue.ID, *groupID)
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}

	// upload attachments to lark group async
	go func() {
		gctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
		defer cancel()

		wg := sync.WaitGroup{}
		for _, att := range issue.Fields.Attachments {
			wg.Add(1)
			go func(attachment *jira.Attachment) {
				defer wg.Done()
				err := c.proxyAttachmentToLarkGroup(gctx, attachment, *cardMsgID)
				if err != nil {
					c.logger.Error(err, "proxy jira attachment to lark group failed", "issue_key", req.IssueKey, "attachment", attachment.Filename)
				}
			}(att)
		}

		wg.Wait()
	}()

	ctrl.Success(ctx, &webhookapi.JiraCreateLarkGroupForS1TicketsResponse{GroupID: groupID})
}

func (c *WebhookController) HandleLarkCardEvent(ctx *gin.Context) {
	cardAction := ctx.MustGet(consts.CtxLarkCardAction).(*larkcard.CardAction)

	if cardAction.Action == nil {
		ctrl.FailWithCode(ctx, fmt.Errorf("received a lark card event but action is nil"), api.ErrCodeInternal)
		return
	}
	cardButtonNameAny, exist := cardAction.Action.Value[consts.LarkCardButtonNameKey]
	if !exist {
		ctrl.FailWithCode(ctx, fmt.Errorf("received a lark card event but button name not found"), api.ErrCodeInternal)
		return
	}
	cardButtonName := cardButtonNameAny.(string)

	// card will be updated back after return ssucces to lark,
	// so we need to handle the action after return success.
	startCh := make(chan struct{})
	defer close(startCh)

	go func() {
		<-startCh

		var err error
		switch cardButtonName {
		case consts.CardButtonUserApproved:
			err = c.handleApproval(context.WithoutCancel(ctx), cardAction, apprent.ApprovalTaskStatusApproved)
		case consts.CardButtonUserRejected:
			err = c.handleApproval(context.WithoutCancel(ctx), cardAction, apprent.ApprovalTaskStatusRejected)
		}
		if err != nil {
			c.logger.Error(err, "handle a card event failed", "card_action", cardAction)
			return
		}
	}()

	ctrl.Success(ctx, nil)
}

func (c *WebhookController) HandleLarkEvent(ctx *gin.Context) {
	val, exist := ctx.Get(consts.CtxLarkEvent)
	if !exist {
		ctrl.FailWithCode(ctx, fmt.Errorf("received a lark event but event is not exist"), api.ErrCodeInternal)
		return
	}
	body := val.(*larkevent.EventV2Body)

	if body.Event == nil {
		ctrl.FailWithCode(ctx, fmt.Errorf("received a lark event but event is nil"), api.ErrCodeInternal)
		return
	}

	err := c.LarkEventDispatcher.Dispatch(ctx, body)
	if err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodeInternal)
		return
	}

	ctrl.Success(ctx, nil)
}

func (c *WebhookController) handleApproval(ctx context.Context, cardEvent *larkcard.CardAction, result apprent.ApprovalTaskStatus) error {
	apprIDAny, exist := cardEvent.Action.Value[consts.LarkApprovalIDKey]
	if !exist {
		return fmt.Errorf("approval id is not found in card action")
	}
	apprID, err := strconv.ParseUint(apprIDAny.(string), 10, 64)
	if err != nil {
		return fmt.Errorf("parse approval id failed: %s", err)
	}

	userResp, err := c.userSvc.ListUsers(ctx, &userv1.ListUsersRequest{
		LarkUserId: cardEvent.UserID,
		Page:       1,
		PerPage:    1,
	})
	if err != nil {
		return fmt.Errorf("lark id %s is not found in user service: %s", cardEvent.UserID, err)
	}
	if len(userResp.Users) == 0 {
		return status.Error(codes.NotFound, fmt.Sprintf("lark id %s is not found in user service", cardEvent.UserID))
	}
	param := apprsvcapi.FinishApprovalTaskParam{
		User:   userResp.Users[0].Email,
		Result: result,
	}
	return c.apprSvc.FinishApprovalTask(ctx, apprID, param)
}

func (c *WebhookController) proxyAttachmentToLarkGroup(ctx context.Context, attach *jira.Attachment, cardMsgID string) error {
	resp, err := c.jiracli.Raw().Issue.DownloadAttachmentWithContext(ctx, attach.ID)
	if err != nil {
		return fmt.Errorf("download attachment %s failed: %s", attach.Filename, err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("download attachment %s failed, status code: %d", attach.Filename, resp.StatusCode)
	}

	var message larkcliim.IMMessage
	switch attach.MimeType {
	case "image/png", "image/jpeg", "image/gif", "image/jpg":
		imgKey, err := c.larkcli.IM.UploadImage(ctx, resp.Body)
		if err != nil {
			return fmt.Errorf("upload attachment %s to lark failed: %s", attach.Filename, err)
		}
		message = larkcliim.IMMessageImage{
			ImageKey: *imgKey,
		}
	case "application/json", "text/plain":
		fileKey, err := c.larkcli.IM.UploadFile(ctx, "stream", attach.Filename, resp.Body)
		if err != nil {
			return fmt.Errorf("upload attachment %s to lark failed: %s", attach.Filename, err)
		}
		message = larkcliim.IMMessageFile{
			FileKey: *fileKey,
		}
	default:
		return fmt.Errorf("attachment %s has unsupported mime type: %s", attach.Filename, attach.MimeType)
	}

	_, err = c.larkcli.IM.ReplyMessage(ctx, cardMsgID, message, true)
	if err != nil {
		return fmt.Errorf("send file message to lark group failed: %s", err)
	}

	return nil
}
