package wechatmini

import (
	"context"
	"crypto/sha1"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
	"time"
)

type Client struct {
	appID      string
	appSecret  string
	devPrefix  string
	httpClient *http.Client
}

type Session struct {
	AppID      string
	OpenID     string
	UnionID    string
	SessionKey string
}

type code2SessionResponse struct {
	OpenID     string `json:"openid"`
	SessionKey string `json:"session_key"`
	UnionID    string `json:"unionid"`
	ErrCode    int    `json:"errcode"`
	ErrMsg     string `json:"errmsg"`
}

func NewClient(appID, appSecret, devPrefix string) *Client {
	return &Client{
		appID:      appID,
		appSecret:  appSecret,
		devPrefix:  devPrefix,
		httpClient: &http.Client{Timeout: 8 * time.Second},
	}
}

func (c *Client) ExchangeCode(ctx context.Context, code string) (*Session, error) {
	code = strings.TrimSpace(code)
	if code == "" {
		return nil, fmt.Errorf("wechat code is required")
	}

	if c.devPrefix != "" && strings.HasPrefix(code, c.devPrefix) {
		suffix := strings.TrimPrefix(code, c.devPrefix)
		if suffix == "" {
			suffix = "default"
		}
		return &Session{
			AppID:   fallbackString(c.appID, "dev-mini-app"),
			OpenID:  "dev_openid_" + normalizeDevID(suffix),
			UnionID: "",
		}, nil
	}

	if c.appID == "" || c.appSecret == "" {
		return nil, fmt.Errorf("wechat mini app credentials are not configured")
	}

	values := url.Values{}
	values.Set("appid", c.appID)
	values.Set("secret", c.appSecret)
	values.Set("js_code", code)
	values.Set("grant_type", "authorization_code")

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.weixin.qq.com/sns/jscode2session?"+values.Encode(), nil)
	if err != nil {
		return nil, err
	}

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	var parsed code2SessionResponse
	if err := json.Unmarshal(body, &parsed); err != nil {
		return nil, err
	}
	if parsed.ErrCode != 0 {
		return nil, fmt.Errorf("wechat code2session failed: %d %s", parsed.ErrCode, parsed.ErrMsg)
	}
	if parsed.OpenID == "" {
		return nil, fmt.Errorf("wechat code2session returned empty openid")
	}

	return &Session{
		AppID:      c.appID,
		OpenID:     parsed.OpenID,
		UnionID:    parsed.UnionID,
		SessionKey: parsed.SessionKey,
	}, nil
}

func normalizeDevID(value string) string {
	sum := sha1.Sum([]byte(value))
	return hex.EncodeToString(sum[:])[:16]
}

func fallbackString(value, fallback string) string {
	if value != "" {
		return value
	}
	return fallback
}
