package lark

import (
	"context"
	"encoding/json"
	"fmt"

	lark "github.com/larksuite/oapi-sdk-go/v3"
	larkcontact "github.com/larksuite/oapi-sdk-go/v3/service/contact/v3"
	larkim "github.com/larksuite/oapi-sdk-go/v3/service/im/v1"

	"github.com/tidbcloud/alertstore/pkg/im"
)

// larkScopeDenied is what Lark returns when the app has not been granted an API
// scope. An operator has to grant it in the developer console, so no amount of
// retrying changes the answer.
const larkScopeDenied = 99991672

// apiError formats a failed Lark call, tagging the one code that a caller must
// not retry. Anything unrecognised stays retryable, which is the safe default
// for delivery.
func apiError(op string, code int, msg string) error {
	if code == larkScopeDenied {
		return fmt.Errorf("%s: %w: code=%d, msg=%s", op, im.ErrPermissionDenied, code, msg)
	}
	return fmt.Errorf("%s: code=%d, msg=%s", op, code, msg)
}

// Client implements im.Provider using Lark/Feishu APIs.
type Client struct {
	client    *lark.Client
	appID     string
	appSecret string
}

// Verify interface compliance at compile time.
var _ im.Provider = (*Client)(nil)

func New(appID, appSecret string) *Client {
	client := lark.NewClient(appID, appSecret)
	return &Client{client: client, appID: appID, appSecret: appSecret}
}

// InspectChannel validates a chat ID without producing a test message.
func (c *Client) InspectChannel(ctx context.Context, channelID string) (*im.Channel, error) {
	chatResp, err := c.client.Im.Chat.Get(ctx, larkim.NewGetChatReqBuilder().ChatId(channelID).Build())
	if err != nil {
		return nil, fmt.Errorf("get chat: %w", err)
	}
	if !chatResp.Success() {
		return nil, apiError("get chat", chatResp.Code, chatResp.Msg)
	}

	memberResp, err := c.client.Im.ChatMembers.IsInChat(ctx,
		larkim.NewIsInChatChatMembersReqBuilder().ChatId(channelID).Build())
	if err != nil {
		return nil, fmt.Errorf("check bot membership: %w", err)
	}
	if !memberResp.Success() {
		return nil, apiError("check bot membership", memberResp.Code, memberResp.Msg)
	}

	channel := &im.Channel{ID: channelID}
	if chatResp.Data != nil && chatResp.Data.Name != nil {
		channel.Name = *chatResp.Data.Name
	}
	if memberResp.Data != nil && memberResp.Data.IsInChat != nil {
		channel.BotIsInChannel = *memberResp.Data.IsInChat
	}
	return channel, nil
}

// listChannelsPageSize is the maximum page size the chat list API accepts.
const listChannelsPageSize = 100

// ListChannels returns the groups the bot belongs to. Lark only lists chats the
// caller is a member of, so membership is implied for every item.
func (c *Client) ListChannels(ctx context.Context) ([]im.Channel, error) {
	var (
		channels  []im.Channel
		pageToken string
	)
	for {
		builder := larkim.NewListChatReqBuilder().PageSize(listChannelsPageSize)
		if pageToken != "" {
			builder = builder.PageToken(pageToken)
		}
		resp, err := c.client.Im.Chat.List(ctx, builder.Build())
		if err != nil {
			return nil, fmt.Errorf("list chats: %w", err)
		}
		if !resp.Success() {
			return nil, apiError("list chats", resp.Code, resp.Msg)
		}
		if resp.Data == nil {
			break
		}
		for _, item := range resp.Data.Items {
			if item.ChatId == nil || *item.ChatId == "" {
				continue
			}
			channel := im.Channel{ID: *item.ChatId, BotIsInChannel: true}
			if item.Name != nil {
				channel.Name = *item.Name
			}
			channels = append(channels, channel)
		}
		if resp.Data.HasMore == nil || !*resp.Data.HasMore ||
			resp.Data.PageToken == nil || *resp.Data.PageToken == "" {
			break
		}
		pageToken = *resp.Data.PageToken
	}
	return channels, nil
}

// SendCardToChannel posts a card to a thread-mode group.
// In thread-mode groups, every top-level message becomes a topic automatically.
func (c *Client) SendCardToChannel(ctx context.Context, channelID string, card *im.Card) (string, string, error) {
	content, err := json.Marshal(buildCardContent(card))
	if err != nil {
		return "", "", fmt.Errorf("marshal card: %w", err)
	}

	req := larkim.NewCreateMessageReqBuilder().
		ReceiveIdType("chat_id").
		Body(larkim.NewCreateMessageReqBodyBuilder().
			ReceiveId(channelID).
			MsgType("interactive").
			Content(string(content)).
			Build()).
		Build()

	resp, err := c.client.Im.Message.Create(ctx, req)
	if err != nil {
		return "", "", fmt.Errorf("send message: %w", err)
	}
	if !resp.Success() {
		return "", "", apiError("send message", resp.Code, resp.Msg)
	}

	var messageID, threadID string
	if resp.Data != nil && resp.Data.MessageId != nil {
		messageID = *resp.Data.MessageId
	}
	if resp.Data != nil && resp.Data.ThreadId != nil {
		threadID = *resp.Data.ThreadId
	}

	return messageID, threadID, nil
}

// ReplyToThread replies within an existing topic/thread.
func (c *Client) ReplyToThread(ctx context.Context, messageID, content string) error {
	req := larkim.NewReplyMessageReqBuilder().
		MessageId(messageID).
		Body(larkim.NewReplyMessageReqBodyBuilder().
			MsgType("text").
			Content(content).
			Build()).
		Build()

	resp, err := c.client.Im.Message.Reply(ctx, req)
	if err != nil {
		return fmt.Errorf("reply to thread: %w", err)
	}
	if !resp.Success() {
		return apiError("reply to thread", resp.Code, resp.Msg)
	}
	return nil
}

// ForwardThread forwards a topic to another group.
// The target group sees a pointer to the original topic and can view subsequent replies.
func (c *Client) ForwardThread(ctx context.Context, threadID, targetChannelID string) error {
	_, err := c.forwardThread(ctx, threadID, targetChannelID, "chat_id")
	return err
}

// ForwardThreadToUser forwards a topic into the bot's direct chat with a user.
func (c *Client) ForwardThreadToUser(ctx context.Context, threadID, userID string) (string, error) {
	return c.forwardThread(ctx, threadID, userID, "open_id")
}

func (c *Client) forwardThread(ctx context.Context, threadID, receiveID, receiveIDType string) (string, error) {
	req := larkim.NewForwardThreadReqBuilder().
		ThreadId(threadID).
		ReceiveIdType(receiveIDType).
		Body(larkim.NewForwardThreadReqBodyBuilder().
			ReceiveId(receiveID).
			Build()).
		Build()

	resp, err := c.client.Im.Thread.Forward(ctx, req)
	if err != nil {
		return "", fmt.Errorf("forward thread: %w", err)
	}
	if !resp.Success() {
		return "", apiError("forward thread", resp.Code, resp.Msg)
	}
	if resp.Data != nil && resp.Data.MessageId != nil {
		return *resp.Data.MessageId, nil
	}
	return "", nil
}

// UpdateCard rewrites an interactive card this app already sent. Lark replaces
// the whole content, so callers pass a freshly rendered card rather than a diff.
func (c *Client) UpdateCard(ctx context.Context, messageID string, card *im.Card) error {
	content, err := json.Marshal(buildCardContent(card))
	if err != nil {
		return fmt.Errorf("marshal card: %w", err)
	}

	req := larkim.NewPatchMessageReqBuilder().
		MessageId(messageID).
		Body(larkim.NewPatchMessageReqBodyBuilder().
			Content(string(content)).
			Build()).
		Build()

	resp, err := c.client.Im.Message.Patch(ctx, req)
	if err != nil {
		return fmt.Errorf("patch card: %w", err)
	}
	if !resp.Success() {
		return apiError("patch card", resp.Code, resp.Msg)
	}
	return nil
}

// SendCard sends a standalone interactive card message to a group.
func (c *Client) SendCard(ctx context.Context, channelID string, card *im.Card) (string, error) {
	cardContent := buildCardContent(card)
	contentJSON, err := json.Marshal(cardContent)
	if err != nil {
		return "", fmt.Errorf("marshal card: %w", err)
	}

	req := larkim.NewCreateMessageReqBuilder().
		ReceiveIdType("chat_id").
		Body(larkim.NewCreateMessageReqBodyBuilder().
			ReceiveId(channelID).
			MsgType("interactive").
			Content(string(contentJSON)).
			Build()).
		Build()

	resp, err := c.client.Im.Message.Create(ctx, req)
	if err != nil {
		return "", fmt.Errorf("send card: %w", err)
	}
	if !resp.Success() {
		return "", apiError("send card", resp.Code, resp.Msg)
	}

	var messageID string
	if resp.Data != nil && resp.Data.MessageId != nil {
		messageID = *resp.Data.MessageId
	}
	return messageID, nil
}

// MakeUrgent sends an in-app buzz notification to specified users.
func (c *Client) MakeUrgent(ctx context.Context, messageID string, userIDs []string) error {
	req := larkim.NewUrgentAppMessageReqBuilder().
		MessageId(messageID).
		UserIdType("open_id").
		UrgentReceivers(larkim.NewUrgentReceiversBuilder().
			UserIdList(userIDs).
			Build()).
		Build()

	resp, err := c.client.Im.Message.UrgentApp(ctx, req)
	if err != nil {
		return fmt.Errorf("make urgent: %w", err)
	}
	if !resp.Success() {
		return apiError("make urgent", resp.Code, resp.Msg)
	}
	return nil
}

// GetReadUsers returns user IDs who have read the message.
func (c *Client) GetReadUsers(ctx context.Context, messageID string) ([]string, error) {
	req := larkim.NewReadUsersMessageReqBuilder().
		MessageId(messageID).
		UserIdType("open_id").
		Build()

	resp, err := c.client.Im.Message.ReadUsers(ctx, req)
	if err != nil {
		return nil, fmt.Errorf("get read users: %w", err)
	}
	if !resp.Success() {
		return nil, apiError("get read users", resp.Code, resp.Msg)
	}

	var userIDs []string
	if resp.Data != nil {
		for _, item := range resp.Data.Items {
			if item.UserId != nil {
				userIDs = append(userIDs, *item.UserId)
			}
		}
	}
	return userIDs, nil
}

// GetUserPhone returns the phone number for a user by their open_id.
func (c *Client) GetUserPhone(ctx context.Context, userID string) (string, error) {
	req := larkcontact.NewGetUserReqBuilder().
		UserId(userID).
		UserIdType("open_id").
		Build()

	resp, err := c.client.Contact.User.Get(ctx, req)
	if err != nil {
		return "", fmt.Errorf("get user: %w", err)
	}
	if !resp.Success() {
		return "", apiError("get user", resp.Code, resp.Msg)
	}

	if resp.Data != nil && resp.Data.User != nil && resp.Data.User.Mobile != nil {
		return *resp.Data.User.Mobile, nil
	}
	return "", fmt.Errorf("no phone number for user %s", userID)
}

// GetUserEmail returns the email for a user by their open_id.
// Prefers enterprise email, falls back to personal email.
func (c *Client) GetUserEmail(ctx context.Context, userID string) (string, error) {
	req := larkcontact.NewGetUserReqBuilder().
		UserId(userID).
		UserIdType("open_id").
		Build()

	resp, err := c.client.Contact.User.Get(ctx, req)
	if err != nil {
		return "", fmt.Errorf("get user: %w", err)
	}
	if !resp.Success() {
		return "", apiError("get user", resp.Code, resp.Msg)
	}

	if resp.Data != nil && resp.Data.User != nil {
		if resp.Data.User.EnterpriseEmail != nil && *resp.Data.User.EnterpriseEmail != "" {
			return *resp.Data.User.EnterpriseEmail, nil
		}
		if resp.Data.User.Email != nil && *resp.Data.User.Email != "" {
			return *resp.Data.User.Email, nil
		}
	}
	return "", fmt.Errorf("no email for user %s", userID)
}

// GetUserName returns the display name for a user by their open_id.
func (c *Client) GetUserName(ctx context.Context, userID string) (string, error) {
	req := larkcontact.NewGetUserReqBuilder().
		UserId(userID).
		UserIdType("open_id").
		Build()

	resp, err := c.client.Contact.User.Get(ctx, req)
	if err != nil {
		return "", fmt.Errorf("get user: %w", err)
	}
	if !resp.Success() {
		return "", apiError("get user", resp.Code, resp.Msg)
	}

	if resp.Data != nil && resp.Data.User != nil && resp.Data.User.Name != nil && *resp.Data.User.Name != "" {
		return *resp.Data.User.Name, nil
	}
	return "", fmt.Errorf("no name for user %s", userID)
}

// GetUserIDByEmail resolves an email address to a Lark open_id.
func (c *Client) GetUserIDByEmail(ctx context.Context, email string) (string, error) {
	req := larkcontact.NewBatchGetIdUserReqBuilder().
		UserIdType("open_id").
		Body(larkcontact.NewBatchGetIdUserReqBodyBuilder().
			Emails([]string{email}).
			Build()).
		Build()

	resp, err := c.client.Contact.User.BatchGetId(ctx, req)
	if err != nil {
		return "", fmt.Errorf("batch get user id: %w", err)
	}
	if !resp.Success() {
		return "", apiError("batch get user id", resp.Code, resp.Msg)
	}

	if resp.Data != nil {
		for _, user := range resp.Data.UserList {
			if user.UserId != nil && *user.UserId != "" {
				return *user.UserId, nil
			}
		}
	}
	// A resigned or unregistered address comes back with an empty user_id.
	return "", fmt.Errorf("%w: email %s", im.ErrUserNotFound, email)
}

// buildCardContent creates a Lark interactive card structure. A card with no
// title gets no header — a reply inside a topic is already under one, and a
// second heading there reads as a separate alert.
func buildCardContent(card *im.Card) map[string]interface{} {
	elements := []interface{}{
		map[string]interface{}{
			"tag": "div",
			"text": map[string]interface{}{
				"content": card.Body,
				"tag":     "lark_md",
			},
		},
	}

	for _, row := range card.ActionRows {
		if len(row) == 0 {
			continue
		}
		buttons := make([]interface{}, 0, len(row))
		for _, action := range row {
			button := map[string]interface{}{
				"tag": "button",
				"text": map[string]interface{}{
					"tag":     "plain_text",
					"content": action.Label,
				},
			}
			if action.URL != "" {
				button["url"] = action.URL
			} else {
				button["value"] = action.Value
			}
			if action.Style != "" {
				button["type"] = action.Style
			}
			buttons = append(buttons, button)
		}
		elements = append(elements, map[string]interface{}{
			"tag":     "action",
			"actions": buttons,
		})
	}

	if card.Note != "" {
		elements = append(elements, map[string]interface{}{
			"tag": "note",
			"elements": []interface{}{
				map[string]interface{}{
					"tag":     "plain_text",
					"content": card.Note,
				},
			},
		})
	}

	content := map[string]interface{}{
		"config": map[string]interface{}{
			"wide_screen_mode": true,
			"enable_forward":   true,
		},
		"elements": elements,
	}

	if card.Title != "" {
		color := card.Color
		if color == "" {
			color = "blue"
		}
		content["header"] = map[string]interface{}{
			"template": color,
			"title": map[string]interface{}{
				"content": card.Title,
				"tag":     "plain_text",
			},
		}
	}
	return content
}
