// Package tencentrtc contains Tencent RTC token and server-side room APIs.
package tencentrtc

import (
	"bytes"
	"context"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"strconv"
	"strings"
	"time"
)

const (
	// DefaultRESTEndpoint is the Tencent Cloud API endpoint for TRTC room management.
	DefaultRESTEndpoint = "trtc.tencentcloudapi.com"
	// DefaultRESTRegion is accepted by TRTC room management APIs and keeps local config explicit.
	DefaultRESTRegion = "ap-guangzhou"

	trtcServiceName            = "trtc"
	trtcAPIVersion             = "2019-07-22"
	removeUserByStrRoomAction  = "RemoveUserByStrRoomId"
	setUserBlockedByStrAction  = "SetUserBlockedByStrRoomId"
	trtcUserMediaEnabled       = 0
	trtcUserAudioDisabled      = 2
	tencentCloudTC3Algorithm   = "TC3-HMAC-SHA256"
	tencentCloudTC3RequestType = "tc3_request"
)

// RESTConfig describes the Tencent Cloud API credentials used for RTC room management.
type RESTConfig struct {
	Enabled bool
	// SDKAppID is the TRTC application ID that owns the room.
	SDKAppID int64
	// SecretID/SecretKey are Tencent Cloud API credentials with permission on the TRTC SDKAppID.
	SecretID  string
	SecretKey string
	// Region is required by Tencent Cloud API even though room IDs remain application-global.
	Region string
	// Endpoint defaults to trtc.tencentcloudapi.com.
	Endpoint string
	// HTTPClient lets tests inject a fake transport and lets services bound public network latency.
	HTTPClient *http.Client
	// Now is injectable so signature tests do not depend on wall clock time.
	Now func() time.Time
}

// RESTClient wraps Tencent Cloud API 3.0 calls needed by room-service.
type RESTClient struct {
	cfg        RESTConfig
	httpClient *http.Client
	now        func() time.Time
}

// NewRESTClient validates the API credential boundary and returns a reusable client.
func NewRESTClient(cfg RESTConfig) (*RESTClient, error) {
	cfg.SecretID = strings.TrimSpace(cfg.SecretID)
	cfg.SecretKey = strings.TrimSpace(cfg.SecretKey)
	cfg.Region = strings.TrimSpace(cfg.Region)
	cfg.Endpoint = strings.TrimSpace(cfg.Endpoint)
	if cfg.SDKAppID <= 0 || cfg.SecretID == "" || cfg.SecretKey == "" {
		return nil, fmt.Errorf("tencent rtc rest config is incomplete")
	}
	if cfg.Region == "" {
		cfg.Region = DefaultRESTRegion
	}
	if cfg.Endpoint == "" {
		cfg.Endpoint = DefaultRESTEndpoint
	}
	if cfg.HTTPClient == nil {
		cfg.HTTPClient = &http.Client{Timeout: 5 * time.Second}
	}
	if cfg.Now == nil {
		cfg.Now = func() time.Time { return time.Now().UTC() }
	}

	return &RESTClient{cfg: cfg, httpClient: cfg.HTTPClient, now: cfg.Now}, nil
}

// RemoveUserByStrRoomID removes a user from a string-ID TRTC room.
// It only affects the live RTC room; room-service must update Room Cell state before calling this.
func (c *RESTClient) RemoveUserByStrRoomID(ctx context.Context, roomID string, userID int64) error {
	roomID = strings.TrimSpace(roomID)
	if roomID == "" || userID <= 0 {
		return fmt.Errorf("rtc remove user is incomplete")
	}

	payload := removeUserByStrRoomRequest{
		SDKAppID: c.cfg.SDKAppID,
		RoomID:   roomID,
		UserIDs:  []string{strconv.FormatInt(userID, 10)},
	}
	return c.post(ctx, removeUserByStrRoomAction, payload)
}

// SetUserAudioBlockedByStrRoomID uses TRTC's server-side media gate instead of
// trusting the target client to stop publishing. Room-service remains the mute
// state owner; this call only projects that committed state onto the live RTC
// data plane and deliberately leaves receiving audio enabled.
func (c *RESTClient) SetUserAudioBlockedByStrRoomID(ctx context.Context, roomID string, userID int64, blocked bool) error {
	roomID = strings.TrimSpace(roomID)
	if roomID == "" || userID <= 0 {
		return fmt.Errorf("rtc set user audio blocked is incomplete")
	}

	muteMode := trtcUserMediaEnabled
	if blocked {
		// IsMute=2 disables only upstream audio. IsMute=1 would also disable
		// video and would silently broaden the semantics of room mute.
		muteMode = trtcUserAudioDisabled
	}
	payload := setUserBlockedByStrRoomRequest{
		SDKAppID: c.cfg.SDKAppID,
		RoomID:   roomID,
		UserID:   strconv.FormatInt(userID, 10),
		IsMute:   muteMode,
	}
	err := c.post(ctx, setUserBlockedByStrAction, payload)
	if IsRESTErrorCode(err, "FailedOperation.RoomNotExist", "FailedOperation.UserNotExist", "InternalError.UserNotExist") {
		// The desired media state is already satisfied when either the room or
		// user has gone. Retrying these terminal absences would only poison the
		// durable room outbox until it reaches the dead-letter threshold.
		return nil
	}
	return err
}

func (c *RESTClient) post(ctx context.Context, action string, payload any) error {
	body, err := json.Marshal(payload)
	if err != nil {
		return err
	}

	request, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://"+strings.TrimSuffix(c.cfg.Endpoint, "/"), bytes.NewReader(body))
	if err != nil {
		return err
	}
	request.Header.Set("Content-Type", "application/json; charset=utf-8")
	request.Header.Set("Host", c.cfg.Endpoint)
	request.Header.Set("X-TC-Action", action)
	request.Header.Set("X-TC-Version", trtcAPIVersion)
	request.Header.Set("X-TC-Region", c.cfg.Region)
	c.sign(request, body, action)

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

	responseBody, err := io.ReadAll(response.Body)
	if err != nil {
		return err
	}
	if response.StatusCode < 200 || response.StatusCode >= 300 {
		return fmt.Errorf("tencent rtc http status %d: %s", response.StatusCode, string(responseBody))
	}

	var cloudResp cloudAPIResponse
	if err := json.Unmarshal(responseBody, &cloudResp); err != nil {
		return err
	}
	if cloudResp.Response.Error != nil {
		return &RESTError{Code: cloudResp.Response.Error.Code, Message: cloudResp.Response.Error.Message}
	}
	return nil
}

// RESTError preserves Tencent's stable business code so callers can classify
// terminal idempotent outcomes without matching a human-readable message.
type RESTError struct {
	Code    string
	Message string
}

func (e *RESTError) Error() string {
	if e == nil {
		return ""
	}
	return fmt.Sprintf("tencent rtc rest failed: code=%s message=%s", e.Code, e.Message)
}

// IsRESTErrorCode reports whether err is a Tencent RTC business error with one
// of the supplied codes. Wrapped errors are supported for service-level context.
func IsRESTErrorCode(err error, codes ...string) bool {
	var restErr *RESTError
	if !errors.As(err, &restErr) {
		return false
	}
	for _, code := range codes {
		if restErr.Code == code {
			return true
		}
	}
	return false
}

func (c *RESTClient) sign(request *http.Request, body []byte, action string) {
	now := c.now().UTC()
	timestamp := now.Unix()
	date := now.Format("2006-01-02")
	request.Header.Set("X-TC-Timestamp", strconv.FormatInt(timestamp, 10))

	hashedPayload := sha256Hex(body)
	canonicalHeaders := "content-type:" + strings.ToLower(request.Header.Get("Content-Type")) + "\n" + "host:" + c.cfg.Endpoint + "\n"
	signedHeaders := "content-type;host"
	canonicalRequest := strings.Join([]string{
		http.MethodPost,
		"/",
		"",
		canonicalHeaders,
		signedHeaders,
		hashedPayload,
	}, "\n")

	credentialScope := date + "/" + trtcServiceName + "/" + tencentCloudTC3RequestType
	stringToSign := strings.Join([]string{
		tencentCloudTC3Algorithm,
		strconv.FormatInt(timestamp, 10),
		credentialScope,
		sha256Hex([]byte(canonicalRequest)),
	}, "\n")
	signature := hex.EncodeToString(hmacSHA256(tc3SigningKey(c.cfg.SecretKey, date), stringToSign))

	authorization := fmt.Sprintf(
		"%s Credential=%s/%s, SignedHeaders=%s, Signature=%s",
		tencentCloudTC3Algorithm,
		c.cfg.SecretID,
		credentialScope,
		signedHeaders,
		signature,
	)
	request.Header.Set("Authorization", authorization)
	_ = action
}

func tc3SigningKey(secretKey string, date string) []byte {
	secretDate := hmacSHA256([]byte("TC3"+secretKey), date)
	secretService := hmacSHA256(secretDate, trtcServiceName)
	return hmacSHA256(secretService, tencentCloudTC3RequestType)
}

func hmacSHA256(key []byte, value string) []byte {
	mac := hmac.New(sha256.New, key)
	_, _ = mac.Write([]byte(value))
	return mac.Sum(nil)
}

func sha256Hex(value []byte) string {
	sum := sha256.Sum256(value)
	return hex.EncodeToString(sum[:])
}

type removeUserByStrRoomRequest struct {
	SDKAppID int64    `json:"SdkAppId"`
	RoomID   string   `json:"RoomId"`
	UserIDs  []string `json:"UserIds"`
}

type setUserBlockedByStrRoomRequest struct {
	SDKAppID int64  `json:"SdkAppId"`
	RoomID   string `json:"StrRoomId"`
	UserID   string `json:"UserId"`
	IsMute   int    `json:"IsMute"`
}

type cloudAPIResponse struct {
	Response struct {
		RequestID string         `json:"RequestId"`
		Error     *cloudAPIError `json:"Error,omitempty"`
	} `json:"Response"`
}

type cloudAPIError struct {
	Code    string `json:"Code"`
	Message string `json:"Message"`
}
