package main

import (
	"bytes"
	"context"
	"crypto/rand"
	"database/sql"
	"encoding/binary"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	_ "github.com/go-sql-driver/mysql"
	"hyapp/pkg/configx"
	"hyapp/pkg/tencentim"
)

const (
	sendC2CMsgCommand = "v4/openim/sendmsg"
	confirmSendValue  = "SEND_REAL_TENCENT_IM_C2C"
)

type serviceConfig struct {
	MySQLDSN  string          `yaml:"mysql_dsn"`
	TencentIM tencentIMConfig `yaml:"tencent_im"`
}

type tencentIMConfig struct {
	Enabled         bool   `yaml:"enabled"`
	SDKAppID        int64  `yaml:"sdk_app_id"`
	SecretKey       string `yaml:"secret_key"`
	AdminIdentifier string `yaml:"admin_identifier"`
	Endpoint        string `yaml:"endpoint"`
}

type sendTextC2CRequest struct {
	SyncOtherMachine int              `json:"SyncOtherMachine"`
	FromAccount      string           `json:"From_Account,omitempty"`
	ToAccount        string           `json:"To_Account"`
	MsgRandom        uint32           `json:"MsgRandom"`
	MsgBody          []messageElement `json:"MsgBody"`
}

type messageElement struct {
	MsgType    string         `json:"MsgType"`
	MsgContent textMsgContent `json:"MsgContent"`
}

type textMsgContent struct {
	Text string `json:"Text"`
}

type restResponse struct {
	ActionStatus string `json:"ActionStatus"`
	ErrorInfo    string `json:"ErrorInfo"`
	ErrorCode    int    `json:"ErrorCode"`
	MsgKey       string `json:"MsgKey,omitempty"`
}

type resultLine struct {
	AtMS      int64  `json:"at_ms"`
	Mode      string `json:"mode"`
	ToAccount string `json:"to_account"`
	Status    string `json:"status"`
	ErrorCode int    `json:"error_code,omitempty"`
	ErrorInfo string `json:"error_info,omitempty"`
	MsgKey    string `json:"msg_key,omitempty"`
}

func main() {
	if err := run(context.Background(), os.Args[1:]); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

func run(ctx context.Context, args []string) error {
	var userConfigPath string
	var imConfigPath string
	var fromAccount string
	var appCode string
	var message string
	var outputPath string
	var limit int
	var sleepMS int
	var timeoutSeconds int
	var send bool
	var confirmSend string
	var includeSender bool

	flags := flag.NewFlagSet("im-c2c-broadcast", flag.ContinueOnError)
	flags.StringVar(&userConfigPath, "user-config", "services/user-service/configs/config.yaml", "user-service YAML config used for mysql_dsn")
	flags.StringVar(&imConfigPath, "im-config", "services/notice-service/configs/config.yaml", "service YAML config used for tencent_im")
	flags.StringVar(&fromAccount, "from-account", "123456", "Tencent IM From_Account")
	flags.StringVar(&appCode, "app-code", "lalu", "users.app_code filter")
	flags.StringVar(&message, "message", "", "plain C2C text message body")
	flags.StringVar(&outputPath, "output", "", "JSONL audit output path")
	flags.IntVar(&limit, "limit", 0, "maximum active users to process; 0 means all")
	flags.IntVar(&sleepMS, "sleep-ms", 25, "sleep between real sends")
	flags.IntVar(&timeoutSeconds, "timeout-seconds", 10, "Tencent REST request timeout")
	flags.BoolVar(&send, "send", false, "perform real Tencent IM sends; omitted means dry-run")
	flags.StringVar(&confirmSend, "confirm-send", "", "must equal "+confirmSendValue+" when --send is set")
	flags.BoolVar(&includeSender, "include-sender", false, "include from-account when it is also an active numeric user")
	if err := flags.Parse(args); err != nil {
		return err
	}

	fromAccount = strings.TrimSpace(fromAccount)
	appCode = strings.TrimSpace(appCode)
	message = strings.TrimSpace(message)
	if fromAccount == "" || appCode == "" || message == "" {
		return errors.New("from-account, app-code and message are required")
	}
	if send && confirmSend != confirmSendValue {
		return fmt.Errorf("--send requires --confirm-send=%s", confirmSendValue)
	}
	if sleepMS < 0 || timeoutSeconds <= 0 || limit < 0 {
		return errors.New("sleep-ms, timeout-seconds and limit are invalid")
	}

	userCfg, err := loadConfig(userConfigPath)
	if err != nil {
		return fmt.Errorf("load user config: %w", err)
	}
	imCfg, err := loadConfig(imConfigPath)
	if err != nil {
		return fmt.Errorf("load im config: %w", err)
	}
	if strings.TrimSpace(userCfg.MySQLDSN) == "" {
		return errors.New("user config mysql_dsn is empty")
	}
	if err := validateTencentIMConfig(imCfg.TencentIM); err != nil {
		return err
	}

	db, err := sql.Open("mysql", userCfg.MySQLDSN)
	if err != nil {
		return err
	}
	defer db.Close()
	if err := db.PingContext(ctx); err != nil {
		return fmt.Errorf("ping mysql: %w", err)
	}

	targets, totalActive, err := listActiveUserIDs(ctx, db, appCode, fromAccount, includeSender, limit)
	if err != nil {
		return err
	}
	mode := "dry-run"
	if send {
		mode = "send"
	}
	if outputPath == "" {
		outputPath = filepath.Join("run", fmt.Sprintf("im-c2c-broadcast-%s.jsonl", time.Now().UTC().Format("20060102-150405")))
	}
	if err := os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil {
		return err
	}
	output, err := os.Create(outputPath)
	if err != nil {
		return err
	}
	defer output.Close()

	fmt.Printf("mode=%s app_code=%s sdk_app_id=%d endpoint=%s from_account=%s total_active=%d targets=%d output=%s\n",
		mode, appCode, imCfg.TencentIM.SDKAppID, firstNonEmpty(imCfg.TencentIM.Endpoint, tencentim.DefaultEndpoint), fromAccount, totalActive, len(targets), outputPath)
	if len(targets) > 0 {
		fmt.Printf("first_target=%s last_target=%s\n", targets[0], targets[len(targets)-1])
	}
	if !send {
		for _, target := range targets {
			if err := writeResult(output, resultLine{AtMS: time.Now().UTC().UnixMilli(), Mode: mode, ToAccount: target, Status: "dry_run"}); err != nil {
				return err
			}
		}
		fmt.Println("dry_run_complete=true")
		return nil
	}

	client := &http.Client{Timeout: time.Duration(timeoutSeconds) * time.Second}
	for i, target := range targets {
		response, err := sendTencentText(ctx, client, imCfg.TencentIM, fromAccount, target, message)
		line := resultLine{AtMS: time.Now().UTC().UnixMilli(), Mode: mode, ToAccount: target, Status: "ok"}
		if err != nil {
			line.Status = "failed"
			line.ErrorInfo = err.Error()
		} else if response.ErrorCode != 0 || !strings.EqualFold(firstNonEmpty(response.ActionStatus, "OK"), "OK") {
			line.Status = "failed"
			line.ErrorCode = response.ErrorCode
			line.ErrorInfo = response.ErrorInfo
		} else {
			line.MsgKey = response.MsgKey
		}
		if err := writeResult(output, line); err != nil {
			return err
		}
		if line.Status != "ok" {
			fmt.Printf("target=%s status=failed error_code=%d error=%s\n", target, line.ErrorCode, line.ErrorInfo)
		}
		if sleepMS > 0 && i+1 < len(targets) {
			time.Sleep(time.Duration(sleepMS) * time.Millisecond)
		}
	}

	fmt.Println("send_complete=true")
	return nil
}

func loadConfig(path string) (serviceConfig, error) {
	var cfg serviceConfig
	err := configx.LoadYAML(strings.TrimSpace(path), &cfg)
	return cfg, err
}

func validateTencentIMConfig(cfg tencentIMConfig) error {
	if cfg.SDKAppID <= 0 || strings.TrimSpace(cfg.SecretKey) == "" || strings.TrimSpace(cfg.AdminIdentifier) == "" {
		return errors.New("tencent_im sdk_app_id, secret_key and admin_identifier are required")
	}
	return nil
}

func listActiveUserIDs(ctx context.Context, db *sql.DB, appCode string, fromAccount string, includeSender bool, limit int) ([]string, int64, error) {
	var totalActive int64
	if err := db.QueryRowContext(ctx, `SELECT COUNT(*) FROM users WHERE app_code = ? AND status = 'active'`, appCode).Scan(&totalActive); err != nil {
		return nil, 0, err
	}

	excludeUserID := int64(0)
	if !includeSender {
		if value, err := strconv.ParseInt(fromAccount, 10, 64); err == nil && value > 0 {
			excludeUserID = value
		}
	}
	query := `SELECT user_id FROM users WHERE app_code = ? AND status = 'active'`
	params := []any{appCode}
	if excludeUserID > 0 {
		query += ` AND user_id <> ?`
		params = append(params, excludeUserID)
	}
	query += ` ORDER BY user_id ASC`
	if limit > 0 {
		query += ` LIMIT ?`
		params = append(params, limit)
	}
	rows, err := db.QueryContext(ctx, query, params...)
	if err != nil {
		return nil, 0, err
	}
	defer rows.Close()

	targets := make([]string, 0)
	for rows.Next() {
		var userID int64
		if err := rows.Scan(&userID); err != nil {
			return nil, 0, err
		}
		targets = append(targets, strconv.FormatInt(userID, 10))
	}
	return targets, totalActive, rows.Err()
}

func sendTencentText(ctx context.Context, client *http.Client, cfg tencentIMConfig, fromAccount string, toAccount string, text string) (restResponse, error) {
	requestBody := sendTextC2CRequest{
		SyncOtherMachine: 2,
		FromAccount:      fromAccount,
		ToAccount:        toAccount,
		MsgRandom:        randomUint32(),
		MsgBody: []messageElement{
			{MsgType: "TIMTextElem", MsgContent: textMsgContent{Text: text}},
		},
	}
	body, err := json.Marshal(requestBody)
	if err != nil {
		return restResponse{}, err
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint(cfg, sendC2CMsgCommand), bytes.NewReader(body))
	if err != nil {
		return restResponse{}, err
	}
	req.Header.Set("Content-Type", "application/json")

	resp, err := client.Do(req)
	if err != nil {
		return restResponse{}, err
	}
	defer resp.Body.Close()
	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return restResponse{}, err
	}
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return restResponse{}, fmt.Errorf("tencent im http status %d: %s", resp.StatusCode, string(respBody))
	}
	var parsed restResponse
	if err := json.Unmarshal(respBody, &parsed); err != nil {
		return restResponse{}, err
	}
	return parsed, nil
}

func endpoint(cfg tencentIMConfig, command string) string {
	adminSig, _ := tencentim.GenerateUserSig(tencentim.UserSigConfig{
		SDKAppID:  cfg.SDKAppID,
		SecretKey: cfg.SecretKey,
		TTL:       24 * time.Hour,
	}, strings.TrimSpace(cfg.AdminIdentifier), time.Now().UTC())

	values := url.Values{}
	values.Set("sdkappid", strconv.FormatInt(cfg.SDKAppID, 10))
	values.Set("identifier", strings.TrimSpace(cfg.AdminIdentifier))
	values.Set("usersig", adminSig.UserSig)
	values.Set("random", strconv.FormatUint(uint64(randomUint32()), 10))
	values.Set("contenttype", "json")

	return "https://" + strings.TrimSuffix(firstNonEmpty(cfg.Endpoint, tencentim.DefaultEndpoint), "/") + "/" + strings.TrimPrefix(command, "/") + "?" + values.Encode()
}

func writeResult(output *os.File, line resultLine) error {
	body, err := json.Marshal(line)
	if err != nil {
		return err
	}
	if _, err := output.Write(append(body, '\n')); err != nil {
		return err
	}
	return nil
}

func randomUint32() uint32 {
	var payload [4]byte
	if _, err := rand.Read(payload[:]); err != nil {
		return uint32(time.Now().UnixNano())
	}
	return binary.BigEndian.Uint32(payload[:])
}

func firstNonEmpty(values ...string) string {
	for _, value := range values {
		value = strings.TrimSpace(value)
		if value != "" {
			return value
		}
	}
	return ""
}
