package weixin

import (
	"bytes"
	"context"
	"crypto/rand"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"net"
	"net/http"
	"strings"
	"time"
)

const (
	defaultBaseURL    = "https://ilinkai.weixin.qq.com"
	defaultCDNBaseURL = "https://novac2c.cdn.weixin.qq.com/c2c"

	defaultLongPollTimeout = 35 * time.Second
	defaultAPITimeout      = 15 * time.Second

	// maxIlinkHTTPResponseBody caps JSON response size (getUpdates may batch many msgs).
	maxIlinkHTTPResponseBody = 64 << 20

	channelVersion = "cc-connect-weixin/1.0"
)

type apiClient struct {
	baseURL    string
	token      string
	routeTag   string
	httpClient *http.Client
}

func newAPIClient(baseURL, token, routeTag string, httpClient *http.Client) *apiClient {
	if strings.TrimSpace(baseURL) == "" {
		baseURL = defaultBaseURL
	}
	baseURL = strings.TrimRight(baseURL, "/") + "/"
	if httpClient == nil {
		httpClient = &http.Client{Timeout: defaultAPITimeout}
	}
	return &apiClient{
		baseURL:    baseURL,
		token:      strings.TrimSpace(token),
		routeTag:   strings.TrimSpace(routeTag),
		httpClient: httpClient,
	}
}

func (c *apiClient) longPollClient(timeout time.Duration) *http.Client {
	if timeout <= 0 {
		timeout = defaultLongPollTimeout
	}
	tr := http.DefaultTransport
	if t, ok := http.DefaultTransport.(*http.Transport); ok {
		cloned := t.Clone()
		tr = cloned
	}
	return &http.Client{
		Timeout:   timeout + 5*time.Second,
		Transport: tr,
	}
}

func randomWechatUIN() string {
	var b [4]byte
	if _, err := rand.Read(b[:]); err != nil {
		return base64.StdEncoding.EncodeToString([]byte("0000"))
	}
	u := uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3])
	return base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%d", u)))
}

func (c *apiClient) post(ctx context.Context, endpoint string, body []byte, timeout time.Duration, label string) ([]byte, error) {
	url := c.baseURL + strings.TrimPrefix(endpoint, "/")
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
	if err != nil {
		return nil, fmt.Errorf("weixin: %s: new request: %w", label, err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("AuthorizationType", "ilink_bot_token")
	req.Header.Set("Content-Length", fmt.Sprintf("%d", len(body)))
	req.Header.Set("X-WECHAT-UIN", randomWechatUIN())
	if c.token != "" {
		req.Header.Set("Authorization", "Bearer "+c.token)
	}
	if c.routeTag != "" {
		req.Header.Set("SKRouteTag", c.routeTag)
	}

	client := c.httpClient
	if timeout > 0 {
		// Dedicated client so long-poll does not inherit short Timeout from default client.
		client = c.longPollClient(timeout)
	}

	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("weixin: %s: %w", label, err)
	}
	defer resp.Body.Close()
	raw, err := io.ReadAll(io.LimitReader(resp.Body, maxIlinkHTTPResponseBody+1))
	if err != nil {
		return nil, fmt.Errorf("weixin: %s: read body: %w", label, err)
	}
	if len(raw) > maxIlinkHTTPResponseBody {
		return nil, fmt.Errorf("weixin: %s: response body exceeds %d bytes", label, maxIlinkHTTPResponseBody)
	}
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("weixin: %s: http %d: %s", label, resp.StatusCode, truncateForLog(raw, 512))
	}
	return raw, nil
}

func truncateForLog(b []byte, max int) string {
	s := string(b)
	if len(s) <= max {
		return s
	}
	return s[:max] + "…"
}

func (c *apiClient) getUpdates(ctx context.Context, buf string, timeoutMs int) (*getUpdatesResp, error) {
	timeout := defaultLongPollTimeout
	if timeoutMs > 0 {
		timeout = time.Duration(timeoutMs) * time.Millisecond
	}
	req := getUpdatesReq{
		GetUpdatesBuf: buf,
		BaseInfo:      baseInfo{ChannelVersion: channelVersion},
	}
	payload, err := json.Marshal(req)
	if err != nil {
		return nil, err
	}
	raw, err := c.post(ctx, "ilink/bot/getupdates", payload, timeout, "getUpdates")
	if err != nil {
		if ctx.Err() != nil {
			return nil, ctx.Err()
		}
		if errors.Is(err, context.DeadlineExceeded) {
			return &getUpdatesResp{Ret: 0, Msgs: nil, GetUpdatesBuf: buf}, nil
		}
		var ne net.Error
		if errors.As(err, &ne) && ne.Timeout() {
			return &getUpdatesResp{Ret: 0, Msgs: nil, GetUpdatesBuf: buf}, nil
		}
		return nil, err
	}
	var out getUpdatesResp
	if err := json.Unmarshal(raw, &out); err != nil {
		return nil, fmt.Errorf("weixin: getUpdates json: %w", err)
	}
	return &out, nil
}

func (c *apiClient) sendMessage(ctx context.Context, msg *sendMessageReq) error {
	if msg == nil {
		return fmt.Errorf("weixin: sendMessage: nil request")
	}
	msg.BaseInfo = baseInfo{ChannelVersion: channelVersion}
	payload, err := json.Marshal(msg)
	if err != nil {
		return err
	}
	raw, err := c.post(ctx, "ilink/bot/sendmessage", payload, 0, "sendMessage")
	if err != nil {
		return err
	}
	if len(bytes.TrimSpace(raw)) == 0 {
		return nil
	}
	var resp sendMessageResp
	if err := json.Unmarshal(raw, &resp); err != nil {
		return fmt.Errorf("weixin: sendMessage: response json: %w: %s", err, truncateForLog(raw, 256))
	}
	if resp.Ret != 0 {
		slog.Warn("weixin: sendMessage declined by API",
			"ret", resp.Ret, "errcode", resp.Errcode, "errmsg", resp.Errmsg,
			"content_len", len(payload))
		return fmt.Errorf("weixin: sendMessage: ret=%d errcode=%d errmsg=%s",
			resp.Ret, resp.Errcode, resp.Errmsg)
	}
	return nil
}

func (c *apiClient) getUploadURL(ctx context.Context, req getUploadURLRequest) (*getUploadURLResponse, error) {
	req.BaseInfo = baseInfo{ChannelVersion: channelVersion}
	payload, err := json.Marshal(req)
	if err != nil {
		return nil, err
	}
	raw, err := c.post(ctx, "ilink/bot/getuploadurl", payload, 0, "getUploadUrl")
	if err != nil {
		return nil, err
	}
	var out getUploadURLResponse
	if err := json.Unmarshal(raw, &out); err != nil {
		return nil, fmt.Errorf("weixin: getUploadUrl json: %w", err)
	}
	// 兼容微信 iLink API 变更：新版返回 upload_full_url 而非 upload_param
	// upload_full_url 是完整的 CDN 上传地址，可独立作为成功路径
	if strings.TrimSpace(out.UploadParam) == "" && strings.TrimSpace(out.UploadFullURL) == "" {
		return nil, fmt.Errorf("weixin: getUploadUrl: empty upload_param and upload_full_url in %s", truncateForLog(raw, 512))
	}
	return &out, nil
}

func (c *apiClient) getConfig(ctx context.Context, userID, contextToken string) (*getConfigResp, error) {
	req := getConfigReq{
		UserID:       userID,
		ContextToken: contextToken,
		BaseInfo:     baseInfo{ChannelVersion: channelVersion},
	}
	payload, err := json.Marshal(req)
	if err != nil {
		return nil, err
	}
	raw, err := c.post(ctx, "ilink/bot/getconfig", payload, 0, "getConfig")
	if err != nil {
		return nil, err
	}
	var out getConfigResp
	if err := json.Unmarshal(raw, &out); err != nil {
		return nil, fmt.Errorf("weixin: getConfig json: %w", err)
	}
	if out.Ret != 0 || out.Errcode != 0 {
		return nil, fmt.Errorf("weixin: getConfig ret=%d errcode=%d errmsg=%s", out.Ret, out.Errcode, out.Errmsg)
	}
	return &out, nil
}

func (c *apiClient) sendTyping(ctx context.Context, userID, typingTicket string, status int) error {
	req := sendTypingReq{
		IlinkUserID:  userID,
		TypingTicket: typingTicket,
		Status:       status,
		BaseInfo:     baseInfo{ChannelVersion: channelVersion},
	}
	payload, err := json.Marshal(req)
	if err != nil {
		return err
	}
	_, err = c.post(ctx, "ilink/bot/sendtyping", payload, 0, "sendTyping")
	return err
}

func (c *apiClient) sendText(ctx context.Context, to, text, contextToken, clientID string) error {
	if strings.TrimSpace(contextToken) == "" {
		return fmt.Errorf("weixin: context_token is required for send")
	}
	items := []messageItem{}
	if strings.TrimSpace(text) != "" {
		items = append(items, messageItem{
			Type:     messageItemText,
			TextItem: &textItem{Text: text},
		})
	}
	if len(items) == 0 {
		return fmt.Errorf("weixin: sendText: empty item_list")
	}
	msg := sendMessageReq{
		Msg: weixinOutboundMsg{
			FromUserID:   "",
			ToUserID:     to,
			ClientID:     clientID,
			MessageType:  messageTypeBot,
			MessageState: messageStateFinish,
			ItemList:     items,
			ContextToken: contextToken,
		},
	}
	return c.sendMessage(ctx, &msg)
}
