package dingbot

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"github.com/eryajf/chatgpt-dingtalk/config"
	"io"
	"mime/multipart"
	"net/http"
	url2 "net/url"
	"sync"
	"time"
)

// OpenAPI doc: https://open.dingtalk.com/document/isvapp/upload-media-files
const (
	MediaTypeImage string = "image"
	MediaTypeVoice string = "voice"
	MediaTypeVideo string = "video"
	MediaTypeFile  string = "file"
)
const (
	MimeTypeImagePng string = "image/png"
)

type MediaUploadResult struct {
	ErrorCode    int64  `json:"errcode"`
	ErrorMessage string `json:"errmsg"`
	MediaID      string `json:"media_id"`
	CreatedAt    int64  `json:"created_at"`
	Type         string `json:"type"`
}

type OAuthTokenResult struct {
	ErrorCode    int    `json:"errcode"`
	ErrorMessage string `json:"errmsg"`
	AccessToken  string `json:"access_token"`
	ExpiresIn    int    `json:"expires_in"`
}

type DingTalkClientInterface interface {
	GetAccessToken() (string, error)
	UploadMedia(content []byte, filename, mediaType, mimeType string) (*MediaUploadResult, error)
}

type DingTalkClientManagerInterface interface {
	GetClientByOAuthClientID(clientId string) DingTalkClientInterface
}

type DingTalkClient struct {
	Credential  config.Credential
	AccessToken string
	expireAt    int64
	mutex       sync.Mutex
}

type DingTalkClientManager struct {
	Credentials []config.Credential
	Clients     map[string]*DingTalkClient
	mutex       sync.Mutex
}

func NewDingTalkClient(credential config.Credential) *DingTalkClient {
	return &DingTalkClient{
		Credential: credential,
	}
}

func NewDingTalkClientManager(conf *config.Configuration) *DingTalkClientManager {
	clients := make(map[string]*DingTalkClient)

	if conf != nil && conf.Credentials != nil {
		for _, credential := range conf.Credentials {
			clients[credential.ClientID] = NewDingTalkClient(credential)
		}
	}
	return &DingTalkClientManager{
		Credentials: conf.Credentials,
		Clients:     clients,
	}
}

func (m *DingTalkClientManager) GetClientByOAuthClientID(clientId string) DingTalkClientInterface {
	m.mutex.Lock()
	defer m.mutex.Unlock()
	if client, ok := m.Clients[clientId]; ok {
		return client
	}
	return nil
}

func (c *DingTalkClient) GetAccessToken() (string, error) {
	accessToken := ""
	{
		// 先查询缓存
		c.mutex.Lock()
		now := time.Now().Unix()
		if c.expireAt > 0 && c.AccessToken != "" && (now+60) < c.expireAt {
			// 预留一分钟有效期避免在Token过期的临界点调用接口出现401错误
			accessToken = c.AccessToken
		}
		c.mutex.Unlock()
	}
	if accessToken != "" {
		return accessToken, nil
	}

	tokenResult, err := c.getAccessTokenFromDingTalk()
	if err != nil {
		return "", err
	}

	{
		// 更新缓存
		c.mutex.Lock()
		c.AccessToken = tokenResult.AccessToken
		c.expireAt = time.Now().Unix() + int64(tokenResult.ExpiresIn)
		c.mutex.Unlock()
	}
	return tokenResult.AccessToken, nil
}

func (c *DingTalkClient) UploadMedia(content []byte, filename, mediaType, mimeType string) (*MediaUploadResult, error) {
	// OpenAPI doc: https://open.dingtalk.com/document/isvapp/upload-media-files
	accessToken, err := c.GetAccessToken()
	if err != nil {
		return nil, err
	}
	if len(accessToken) == 0 {
		return nil, errors.New("empty access token")
	}
	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)
	part, err := writer.CreateFormFile("media", filename)
	if err != nil {
		return nil, err
	}
	_, err = part.Write(content)
	writer.WriteField("type", mediaType)
	err = writer.Close()
	if err != nil {
		return nil, err
	}

	// Create a new HTTP request to upload the media file
	url := fmt.Sprintf("https://oapi.dingtalk.com/media/upload?access_token=%s", url2.QueryEscape(accessToken))
	req, err := http.NewRequest("POST", url, body)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", writer.FormDataContentType())

	// Send the HTTP request and parse the response
	client := &http.Client{
		Timeout: time.Second * 60,
	}
	res, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()

	// Parse the response body as JSON and extract the media ID
	media := &MediaUploadResult{}
	bodyBytes, err := io.ReadAll(res.Body)
	json.Unmarshal(bodyBytes, media)
	if err != nil {
		return nil, err
	}
	if media.ErrorCode != 0 {
		return nil, errors.New(media.ErrorMessage)
	}
	return media, nil
}

func (c *DingTalkClient) getAccessTokenFromDingTalk() (*OAuthTokenResult, error) {
	// OpenAPI doc: https://open.dingtalk.com/document/orgapp/obtain-orgapp-token
	apiUrl := "https://oapi.dingtalk.com/gettoken"
	queryParams := url2.Values{}
	queryParams.Add("appkey", c.Credential.ClientID)
	queryParams.Add("appsecret", c.Credential.ClientSecret)

	// Create a new HTTP request to get the AccessToken
	req, err := http.NewRequest("GET", apiUrl+"?"+queryParams.Encode(), nil)
	if err != nil {
		return nil, err
	}

	// Send the HTTP request and parse the response body as JSON
	client := http.Client{
		Timeout: time.Second * 60,
	}
	res, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()
	body, err := io.ReadAll(res.Body)
	if err != nil {
		return nil, err
	}
	tokenResult := &OAuthTokenResult{}
	err = json.Unmarshal(body, tokenResult)
	if err != nil {
		return nil, err
	}
	if tokenResult.ErrorCode != 0 {
		return nil, errors.New(tokenResult.ErrorMessage)
	}
	return tokenResult, nil
}