package larkcli

import (
	"context"
	"errors"
	"fmt"
	"sync"
	"time"

	"github.com/tidbcloud/aisupport/packages/sdk-go/infra/config"
	"github.com/tidbcloud/aisupport/packages/sdk-go/infra/lark/client/chat"
	"github.com/tidbcloud/aisupport/packages/sdk-go/infra/lark/client/contact"
	"github.com/tidbcloud/aisupport/packages/sdk-go/infra/lark/client/document"
	"github.com/tidbcloud/aisupport/packages/sdk-go/infra/lark/client/drive"
	"github.com/tidbcloud/aisupport/packages/sdk-go/infra/lark/client/im"
	"github.com/tidbcloud/aisupport/packages/sdk-go/infra/lark/client/meeting"
	"github.com/tidbcloud/aisupport/packages/sdk-go/infra/lark/client/task"
	"github.com/tidbcloud/aisupport/packages/sdk-go/infra/lark/token"

	"github.com/go-resty/resty/v2"
	lark "github.com/larksuite/oapi-sdk-go/v3"
	larkcore "github.com/larksuite/oapi-sdk-go/v3/core"
)

type BotInfo struct {
	AppName string
	OpenID  string
}

type LarkClient struct {
	Conf       config.LarkClient
	BotInfo    BotInfo
	Raw        *lark.Client
	Contact    contact.Contact
	Task       task.Task
	Chat       chat.Chat
	Document   document.Document
	Drive      drive.Drive
	IM         im.IM
	Meeting    meeting.Meeting
	TokenStore token.TokenStore
	resty      *resty.Client

	tokenMu                 sync.Mutex
	tenantAccessToken       string
	tenantAccessTokenExpiry time.Time
}

const tenantAccessTokenRefreshLead = 5 * time.Minute

func New(ctx context.Context, conf config.LarkClient) (*LarkClient, error) {
	raw := lark.NewClient(conf.AppCode, conf.AppSecret)

	client := &LarkClient{
		Conf:       conf,
		Raw:        raw,
		Contact:    contact.New(raw),
		Task:       task.New(raw),
		Chat:       chat.New(raw),
		Document:   document.New(raw, conf.BaseURL),
		Drive:      drive.New(raw),
		IM:         im.New(raw),
		Meeting:    meeting.New(raw),
		TokenStore: token.NewMem(),
		resty: resty.New().
			SetBaseURL("https://open.feishu.cn").
			SetTimeout(10 * time.Second),
	}

	botInfo, err := client.GetBotInfo(ctx)
	if err != nil {
		return nil, err
	}
	client.BotInfo = botInfo

	return client, nil
}

func (c *LarkClient) AsUser(email string) (larkcore.RequestOptionFunc, error) {
	token, err := c.TokenStore.Get(context.Background(), email)
	if err != nil {
		return nil, fmt.Errorf("get user token failed: %w", err)
	}

	return func(option *larkcore.RequestOption) {
		option.UserAccessToken = token.AccessToken
	}, nil
}

// TenantAccessToken returns a valid tenant_access_token for the configured
// self-built app. The token is cached locally and refreshed before expiry so
// callers can inject it into subprocess environments without reimplementing
// Lark auth.
func (c *LarkClient) TenantAccessToken(ctx context.Context) (string, error) {
	now := time.Now()
	c.tokenMu.Lock()
	if c.tenantAccessToken != "" && c.tenantAccessTokenExpiry.After(now.Add(tenantAccessTokenRefreshLead)) {
		token := c.tenantAccessToken
		c.tokenMu.Unlock()
		return token, nil
	}
	c.tokenMu.Unlock()

	resp, err := c.Raw.GetTenantAccessTokenBySelfBuiltApp(ctx, &larkcore.SelfBuiltTenantAccessTokenReq{
		AppID:     c.Conf.AppCode,
		AppSecret: c.Conf.AppSecret,
	})
	if err != nil {
		return "", fmt.Errorf("get lark tenant access token: %w", err)
	}
	if !resp.Success() {
		return "", fmt.Errorf("get lark tenant access token: %s", resp.CodeError.String())
	}
	if resp.TenantAccessToken == "" {
		return "", errors.New("get lark tenant access token: empty token")
	}

	expiresAt := now.Add(time.Duration(resp.Expire) * time.Second)
	c.tokenMu.Lock()
	c.tenantAccessToken = resp.TenantAccessToken
	c.tenantAccessTokenExpiry = expiresAt
	c.tokenMu.Unlock()
	return resp.TenantAccessToken, nil
}

func (c *LarkClient) GetBotInfo(ctx context.Context) (BotInfo, error) {
	token, err := c.TenantAccessToken(ctx)
	if err != nil {
		return BotInfo{}, err
	}

	payload := struct {
		Code int    `json:"code"`
		Msg  string `json:"msg"`
		Bot  struct {
			AppName string `json:"app_name"`
			OpenID  string `json:"open_id"`
		} `json:"bot"`
	}{}

	resp, err := c.resty.R().
		SetContext(ctx).
		SetAuthToken(token).
		SetResult(&payload).
		Get("/open-apis/bot/v3/info")
	if err != nil {
		return BotInfo{}, fmt.Errorf("get lark bot info: %w", err)
	}
	if !resp.IsSuccess() {
		return BotInfo{}, fmt.Errorf("get lark bot info: status %s, body: %s", resp.Status(), string(resp.Body()))
	}

	return BotInfo{
		AppName: payload.Bot.AppName,
		OpenID:  payload.Bot.OpenID,
	}, nil
}
